Skip to main content

cow_composable/
twap.rs

1//! `TWAP` (Time-Weighted Average Price) conditional order and composable-order utilities.
2
3use std::fmt;
4
5use alloy_primitives::{Address, B256, U256, keccak256};
6use cow_errors::CowError;
7use cow_types::OrderKind;
8
9use super::types::{
10    ConditionalOrderParams, DurationOfPart, PollResult, TWAP_HANDLER_ADDRESS, TwapData,
11    TwapStartTime, TwapStruct,
12};
13
14/// A `TWAP` order ready to be submitted to `ComposableCow`.
15#[derive(Debug, Clone)]
16pub struct TwapOrder {
17    /// The underlying `TWAP` configuration.
18    pub data: TwapData,
19    /// 32-byte salt uniquely identifying this order instance.
20    pub salt: B256,
21}
22
23impl TwapOrder {
24    /// Create a new `TWAP` order.
25    ///
26    /// The salt is derived deterministically from the order parameters.
27    /// Use [`TwapOrder::with_salt`] to supply an explicit salt.
28    ///
29    /// # Example
30    ///
31    /// ```rust
32    /// use alloy_primitives::{Address, U256};
33    /// use cow_composable::{TwapData, TwapOrder};
34    ///
35    /// let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600);
36    /// let order = TwapOrder::new(data);
37    /// assert_eq!(order.data.num_parts, 4);
38    /// assert_eq!(order.data.part_duration, 3600);
39    /// ```
40    #[must_use]
41    pub fn new(data: TwapData) -> Self {
42        let salt = deterministic_salt(&data);
43        Self { data, salt }
44    }
45
46    /// Create a `TWAP` order with an explicit salt.
47    ///
48    /// # Arguments
49    ///
50    /// * `data` - The `TWAP` order configuration.
51    /// * `salt` - A caller-chosen 32-byte salt to uniquely identify this order.
52    ///
53    /// # Returns
54    ///
55    /// A [`TwapOrder`] using the provided salt verbatim.
56    #[must_use]
57    pub const fn with_salt(data: TwapData, salt: B256) -> Self {
58        Self { data, salt }
59    }
60
61    /// Returns the on-chain [`ConditionalOrderParams`] for this order.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`CowError::AppData`] if ABI encoding fails.
66    pub fn to_params(&self) -> Result<ConditionalOrderParams, CowError> {
67        Ok(ConditionalOrderParams {
68            handler: TWAP_HANDLER_ADDRESS,
69            salt: self.salt,
70            static_input: encode_twap_static_input(&self.data)?,
71        })
72    }
73
74    /// Unique order ID: `keccak256(abi.encode(ConditionalOrderParams))`.
75    ///
76    /// # Errors
77    ///
78    /// Returns [`CowError::AppData`] if encoding fails.
79    pub fn id(&self) -> Result<B256, CowError> {
80        Ok(order_id(&self.to_params()?))
81    }
82
83    /// Validate the order parameters.
84    ///
85    /// Mirrors the `TypeScript` SDK `Twap.isValid()` logic:
86    /// - Tokens must differ and must not be the zero address
87    /// - `sell_amount` and `buy_amount` must be non-zero
88    /// - `num_parts` must be ≥ 2
89    /// - `part_duration` must be > 0 and ≤ [`MAX_FREQUENCY`](super::types::MAX_FREQUENCY)
90    /// - `sell_amount` must be divisible by `num_parts`
91    /// - If [`DurationOfPart::LimitDuration`]: `duration` must be ≤ `part_duration`
92    #[must_use]
93    pub fn is_valid(&self) -> bool {
94        use super::types::MAX_FREQUENCY;
95        let d = &self.data;
96        if d.sell_token == d.buy_token {
97            return false;
98        }
99        if d.sell_token.is_zero() || d.buy_token.is_zero() {
100            return false;
101        }
102        if d.sell_amount.is_zero() || d.buy_amount.is_zero() {
103            return false;
104        }
105        if d.num_parts < 2 {
106            return false;
107        }
108        if d.part_duration == 0 || d.part_duration > MAX_FREQUENCY {
109            return false;
110        }
111        if !(d.sell_amount % U256::from(d.num_parts)).is_zero() {
112            return false;
113        }
114        if let DurationOfPart::LimitDuration { duration } = d.duration_of_part &&
115            duration > d.part_duration
116        {
117            return false;
118        }
119        true
120    }
121
122    /// Return the per-part sell and buy amounts `(part_sell, min_part_buy)`.
123    ///
124    /// These are the amounts used in each individual order slice:
125    /// `sell_amount / num_parts` and `buy_amount / num_parts`.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`CowError::AppData`] if `num_parts` is zero.
130    ///
131    /// # Example
132    ///
133    /// ```rust
134    /// use alloy_primitives::{Address, U256};
135    /// use cow_composable::{TwapData, TwapOrder};
136    ///
137    /// let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600)
138    ///     .with_buy_amount(U256::from(800u64));
139    /// let order = TwapOrder::new(data);
140    /// let (sell, buy) = order.per_part_amounts().unwrap();
141    /// assert_eq!(sell, U256::from(250u64));
142    /// assert_eq!(buy, U256::from(200u64));
143    /// ```
144    #[allow(clippy::type_complexity, reason = "two-element tuple is readable as-is")]
145    pub fn per_part_amounts(&self) -> Result<(U256, U256), CowError> {
146        let n = self.data.num_parts;
147        if n == 0 {
148            return Err(CowError::AppData("num_parts must be > 0".into()));
149        }
150        let divisor = U256::from(n);
151        Ok((self.data.sell_amount / divisor, self.data.buy_amount / divisor))
152    }
153
154    /// Convert this order's user-facing data into the on-chain [`TwapStruct`] representation.
155    ///
156    /// The struct uses per-part amounts and the raw `span`/`t0` fields.
157    ///
158    /// # Errors
159    ///
160    /// Returns [`CowError::AppData`] if `num_parts` is zero.
161    pub fn to_struct(&self) -> Result<TwapStruct, CowError> {
162        data_to_struct(&self.data)
163    }
164
165    /// Check tradability of this `TWAP` order at the given block timestamp.
166    ///
167    /// Returns [`PollResult::Success`] if the order is within its execution
168    /// window, [`PollResult::TryAtEpoch`] if the order has not started yet,
169    /// or [`PollResult::DontTryAgain`] if the order has fully expired.
170    ///
171    /// For [`TwapStartTime::AtMiningTime`] orders, always returns
172    /// [`PollResult::Success`] because the start time is not known until mined.
173    #[must_use]
174    pub fn poll_validate(&self, block_timestamp: u64) -> PollResult {
175        let d = &self.data;
176        let start = match d.start_time {
177            TwapStartTime::AtMiningTime => {
178                return PollResult::Success { order: None, signature: None };
179            }
180            TwapStartTime::At(ts) => u64::from(ts),
181        };
182        let end = start + u64::from(d.num_parts) * u64::from(d.part_duration);
183
184        if block_timestamp < start {
185            return PollResult::TryAtEpoch { epoch: start };
186        }
187        if block_timestamp >= end {
188            return PollResult::DontTryAgain { reason: "TWAP order has fully expired".into() };
189        }
190        PollResult::Success { order: None, signature: None }
191    }
192}
193
194impl TwapOrder {
195    /// Returns a reference to the 32-byte salt.
196    ///
197    /// # Returns
198    ///
199    /// A shared reference to the [`B256`] salt that uniquely identifies this
200    /// order instance within a `ComposableCow` safe.
201    #[must_use]
202    pub const fn salt_ref(&self) -> &B256 {
203        &self.salt
204    }
205
206    /// Returns a reference to the underlying [`TwapData`].
207    ///
208    /// # Returns
209    ///
210    /// A shared reference to the [`TwapData`] configuration backing this order.
211    #[must_use]
212    pub const fn data_ref(&self) -> &TwapData {
213        &self.data
214    }
215
216    /// Returns the total sell amount across all parts.
217    ///
218    /// ```
219    /// use alloy_primitives::{Address, U256};
220    /// use cow_composable::{TwapData, TwapOrder};
221    ///
222    /// let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1_000u64), 4, 3_600);
223    /// let order = TwapOrder::new(data);
224    /// assert_eq!(order.total_sell_amount(), U256::from(1_000u64));
225    /// ```
226    #[must_use]
227    pub const fn total_sell_amount(&self) -> U256 {
228        self.data.sell_amount
229    }
230
231    /// Returns the total minimum buy amount across all parts.
232    ///
233    /// ```
234    /// use alloy_primitives::{Address, U256};
235    /// use cow_composable::{TwapData, TwapOrder};
236    ///
237    /// let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3_600)
238    ///     .with_buy_amount(U256::from(800u64));
239    /// let order = TwapOrder::new(data);
240    /// assert_eq!(order.total_buy_amount(), U256::from(800u64));
241    /// ```
242    #[must_use]
243    pub const fn total_buy_amount(&self) -> U256 {
244        self.data.buy_amount
245    }
246
247    /// Returns `true` if this is a sell-direction `TWAP` order.
248    ///
249    /// # Returns
250    ///
251    /// `true` when the order kind is [`OrderKind::Sell`], `false` otherwise.
252    #[must_use]
253    pub const fn is_sell(&self) -> bool {
254        self.data.is_sell()
255    }
256
257    /// Returns `true` if this is a buy-direction `TWAP` order.
258    ///
259    /// # Returns
260    ///
261    /// `true` when the order kind is [`OrderKind::Buy`], `false` otherwise.
262    #[must_use]
263    pub const fn is_buy(&self) -> bool {
264        self.data.is_buy()
265    }
266
267    /// Returns `true` if the order has fully expired at the given Unix timestamp.
268    ///
269    /// Delegates to [`TwapData::is_expired`]. Returns `false` when the start
270    /// time is [`TwapStartTime::AtMiningTime`] (end time is unknown until mined).
271    ///
272    /// ```
273    /// use alloy_primitives::{Address, U256};
274    /// use cow_composable::{TwapData, TwapOrder, TwapStartTime};
275    ///
276    /// let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3_600)
277    ///     .with_start_time(TwapStartTime::At(1_000_000));
278    /// // ends at 1_000_000 + 4 × 3600 = 1_014_400
279    /// let order = TwapOrder::new(data);
280    /// assert!(!order.is_expired_at(1_014_399));
281    /// assert!(order.is_expired_at(1_014_400));
282    /// ```
283    #[must_use]
284    pub const fn is_expired_at(&self, block_timestamp: u64) -> bool {
285        self.data.is_expired(block_timestamp)
286    }
287
288    /// Return the fixed start timestamp, or `None` when the order starts at mining time.
289    ///
290    /// Mirrors [`TwapData::start_time`]`::timestamp()`.
291    #[must_use]
292    pub const fn start_timestamp(&self) -> Option<u32> {
293        self.data.start_time.timestamp()
294    }
295}
296
297impl fmt::Display for TwapOrder {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        fmt::Display::fmt(&self.data, f)
300    }
301}
302
303/// Compute the on-chain order ID from [`ConditionalOrderParams`].
304///
305/// The order ID is `keccak256(abi.encode(handler, salt, staticInput))` where
306/// `staticInput` is encoded as a dynamic `bytes` field with a 32-byte length
307/// prefix, zero-padded to a 32-byte boundary.
308///
309/// Mirrors `ConditionalOrder.id` in the `TypeScript` SDK.
310///
311/// # Example
312///
313/// ```rust
314/// use alloy_primitives::{Address, B256};
315/// use cow_composable::{ConditionalOrderParams, order_id};
316///
317/// let params = ConditionalOrderParams {
318///     handler: Address::ZERO,
319///     salt: B256::ZERO,
320///     static_input: vec![0xab, 0xcd],
321/// };
322/// let id = order_id(&params);
323/// assert_ne!(id, B256::ZERO);
324/// ```
325#[must_use]
326pub fn order_id(params: &ConditionalOrderParams) -> B256 {
327    let mut buf = Vec::with_capacity(4 * 32 + pad32_len(params.static_input.len()));
328    buf.extend_from_slice(&pad_address(params.handler.as_slice()));
329    buf.extend_from_slice(params.salt.as_slice());
330    buf.extend_from_slice(&u256_be(96u64));
331    buf.extend_from_slice(&u256_be(params.static_input.len() as u64));
332    pad_into(&mut buf, &params.static_input);
333    keccak256(&buf)
334}
335
336/// ABI-encode [`ConditionalOrderParams`] into a `0x`-prefixed hex string.
337///
338/// Encodes `(address handler, bytes32 salt, bytes staticInput)` using the
339/// standard ABI tuple encoding used by `ComposableCow` watchtowers. The
340/// `staticInput` field is encoded as dynamic `bytes` with a 32-byte length
341/// prefix, zero-padded to a 32-byte boundary.
342///
343/// Decode with [`decode_params`]. Mirrors `encodeParams` from the `TypeScript`
344/// SDK's composable utils.
345///
346/// # Example
347///
348/// ```rust
349/// use alloy_primitives::Address;
350/// use cow_composable::{ConditionalOrderParams, encode_params};
351///
352/// let params = ConditionalOrderParams {
353///     handler: Address::ZERO,
354///     salt: alloy_primitives::B256::ZERO,
355///     static_input: vec![0xab, 0xcd],
356/// };
357/// let hex = encode_params(&params);
358/// assert!(hex.starts_with("0x"));
359/// assert_eq!(hex.len(), 2 + 2 * (5 * 32)); // head (3 × 32) + len (32) + padded data (32)
360/// ```
361#[must_use]
362pub fn encode_params(params: &ConditionalOrderParams) -> String {
363    let static_len = params.static_input.len();
364    let padded_len = pad32_len(static_len);
365    let mut buf = Vec::with_capacity(4 * 32 + padded_len);
366    buf.extend_from_slice(&pad_address(params.handler.as_slice()));
367    buf.extend_from_slice(params.salt.as_slice());
368    buf.extend_from_slice(&u256_be(96u64)); // offset to dynamic bytes = 3 * 32
369    buf.extend_from_slice(&u256_be(static_len as u64));
370    pad_into(&mut buf, &params.static_input);
371    format!("0x{}", alloy_primitives::hex::encode(&buf))
372}
373
374/// ABI-decode a hex string into [`ConditionalOrderParams`].
375///
376/// Reverses [`encode_params`]: reads `(address, bytes32, bytes)` from a
377/// `0x`-prefixed hex string.  Mirrors `decodeParams` from the `TypeScript` SDK.
378///
379/// # Errors
380///
381/// Returns [`CowError::AppData`] if the hex is invalid or the data is too short.
382///
383/// # Example
384///
385/// ```rust
386/// use alloy_primitives::Address;
387/// use cow_composable::{ConditionalOrderParams, decode_params, encode_params};
388///
389/// let params = ConditionalOrderParams {
390///     handler: Address::ZERO,
391///     salt: alloy_primitives::B256::ZERO,
392///     static_input: vec![0xde, 0xad],
393/// };
394/// let encoded = encode_params(&params);
395/// let decoded = decode_params(&encoded).unwrap();
396/// assert_eq!(decoded.handler, params.handler);
397/// assert_eq!(decoded.salt, params.salt);
398/// assert_eq!(decoded.static_input, params.static_input);
399/// ```
400pub fn decode_params(hex: &str) -> Result<ConditionalOrderParams, CowError> {
401    let stripped = hex.trim_start_matches("0x");
402    let bytes = alloy_primitives::hex::decode(stripped)
403        .map_err(|e| CowError::AppData(format!("decode_params hex: {e}")))?;
404    if bytes.len() < 4 * 32 {
405        return Err(CowError::AppData(format!(
406            "decode_params: too short ({} bytes, need ≥ 128)",
407            bytes.len()
408        )));
409    }
410    let mut handler_bytes = [0u8; 20];
411    handler_bytes.copy_from_slice(&bytes[12..32]);
412    let handler = Address::new(handler_bytes);
413
414    let mut salt_bytes = [0u8; 32];
415    salt_bytes.copy_from_slice(&bytes[32..64]);
416    let salt = B256::new(salt_bytes);
417
418    // bytes[64..96] = offset (should be 96, but we tolerate any valid offset)
419    let data_len = usize::try_from(U256::from_be_slice(&bytes[96..128]))
420        .map_err(|_e| CowError::AppData("decode_params: static_input length overflow".into()))?;
421    let data_end = 128usize
422        .checked_add(data_len)
423        .ok_or_else(|| CowError::AppData("decode_params: static_input length overflow".into()))?;
424    if bytes.len() < data_end {
425        return Err(CowError::AppData(format!(
426            "decode_params: data truncated (need {data_end} bytes, have {})",
427            bytes.len()
428        )));
429    }
430    let static_input = bytes[128..data_end].to_vec();
431    Ok(ConditionalOrderParams { handler, salt, static_input })
432}
433
434/// Format a Unix timestamp as an `RFC 3339` / `ISO 8601` date-time string.
435///
436/// Mirrors `formatEpoch` from the `TypeScript` SDK composable utils.
437///
438/// # Example
439///
440/// ```rust
441/// use cow_composable::format_epoch;
442///
443/// let s = format_epoch(1_700_000_000);
444/// assert!(s.starts_with("2023-11-14"));
445/// ```
446#[must_use]
447pub fn format_epoch(epoch: u32) -> String {
448    use chrono::{DateTime, Utc};
449    DateTime::<Utc>::from_timestamp(i64::from(epoch), 0)
450        .map_or_else(|| format!("{epoch}"), |dt| dt.to_rfc3339())
451}
452
453// ── ABI encoding ──────────────────────────────────────────────────────────────
454
455/// ABI-encode the `TWAP` static input from user-facing [`TwapData`].
456///
457/// Converts total amounts to per-part via [`data_to_struct`], then encodes
458/// the resulting [`TwapStruct`] as a 320-byte ABI tuple:
459///
460/// ```text
461/// (address sellToken, address buyToken, address receiver,
462///  uint256 partSellAmount, uint256 minPartLimit,
463///  uint32 t0, uint32 n, uint32 t, uint32 span,
464///  bytes32 appData)
465/// ```
466///
467/// Note: the contract takes **per-part** amounts, not totals.
468///
469/// # Arguments
470///
471/// * `d` - The user-facing `TWAP` order data with total amounts.
472///
473/// # Returns
474///
475/// A 320-byte `Vec<u8>` containing the ABI-encoded `TWAP` static input,
476/// or a [`CowError::AppData`] if `num_parts` is zero.
477fn encode_twap_static_input(d: &TwapData) -> Result<Vec<u8>, CowError> {
478    let s = data_to_struct(d)?;
479    Ok(encode_struct(&s))
480}
481
482/// Convert [`TwapData`] (user-facing, total amounts) into a [`TwapStruct`] (per-part amounts).
483///
484/// Divides `sell_amount` and `buy_amount` by `num_parts` to produce
485/// `part_sell_amount` and `min_part_limit`.  Maps [`TwapStartTime`] and
486/// [`DurationOfPart`] to their raw `t0` / `span` `u32` representations.
487///
488/// This is the inverse of [`struct_to_data`].
489///
490/// # Errors
491///
492/// Returns [`CowError::AppData`] if `num_parts` is zero.
493///
494/// # Example
495///
496/// ```rust
497/// use alloy_primitives::{Address, U256};
498/// use cow_composable::{TwapData, data_to_struct};
499///
500/// let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600)
501///     .with_buy_amount(U256::from(800u64));
502/// let s = data_to_struct(&data).unwrap();
503/// assert_eq!(s.part_sell_amount, U256::from(250u64));
504/// assert_eq!(s.min_part_limit, U256::from(200u64));
505/// assert_eq!(s.n, 4);
506/// assert_eq!(s.t, 3600);
507/// ```
508pub fn data_to_struct(d: &TwapData) -> Result<TwapStruct, CowError> {
509    if d.num_parts == 0 {
510        return Err(CowError::AppData("num_parts must be > 0".into()));
511    }
512    let n = U256::from(d.num_parts);
513    Ok(TwapStruct {
514        sell_token: d.sell_token,
515        buy_token: d.buy_token,
516        receiver: d.receiver,
517        part_sell_amount: d.sell_amount / n,
518        min_part_limit: d.buy_amount / n,
519        t0: match d.start_time {
520            TwapStartTime::AtMiningTime => 0,
521            TwapStartTime::At(ts) => ts,
522        },
523        n: d.num_parts,
524        t: d.part_duration,
525        span: match d.duration_of_part {
526            DurationOfPart::Auto => 0,
527            DurationOfPart::LimitDuration { duration } => duration,
528        },
529        app_data: d.app_data,
530    })
531}
532
533/// Convert a [`TwapStruct`] (per-part, on-chain view) back into [`TwapData`].
534///
535/// Multiplies `part_sell_amount` and `min_part_limit` by `n` to recover total
536/// amounts. Maps `t0` and `span` back into [`TwapStartTime`] and
537/// [`DurationOfPart`] enums. Sets `kind` to [`OrderKind::Sell`] and
538/// `partially_fillable` to `false` (these fields are not encoded on-chain).
539///
540/// This is the inverse of [`data_to_struct`].
541///
542/// # Example
543///
544/// ```rust
545/// use alloy_primitives::{Address, B256, U256};
546/// use cow_composable::{TwapStruct, struct_to_data};
547///
548/// let s = TwapStruct {
549///     sell_token: Address::ZERO,
550///     buy_token: Address::ZERO,
551///     receiver: Address::ZERO,
552///     part_sell_amount: U256::from(250u64),
553///     min_part_limit: U256::from(200u64),
554///     t0: 1_000_000,
555///     n: 4,
556///     t: 3600,
557///     span: 0,
558///     app_data: B256::ZERO,
559/// };
560/// let data = struct_to_data(&s);
561/// assert_eq!(data.sell_amount, U256::from(1000u64));
562/// assert_eq!(data.buy_amount, U256::from(800u64));
563/// assert_eq!(data.num_parts, 4);
564/// ```
565#[must_use]
566pub fn struct_to_data(s: &TwapStruct) -> TwapData {
567    TwapData {
568        sell_token: s.sell_token,
569        buy_token: s.buy_token,
570        receiver: s.receiver,
571        sell_amount: s.part_sell_amount * U256::from(s.n),
572        buy_amount: s.min_part_limit * U256::from(s.n),
573        start_time: if s.t0 == 0 { TwapStartTime::AtMiningTime } else { TwapStartTime::At(s.t0) },
574        part_duration: s.t,
575        num_parts: s.n,
576        app_data: s.app_data,
577        partially_fillable: false,
578        kind: OrderKind::Sell,
579        duration_of_part: if s.span == 0 {
580            DurationOfPart::Auto
581        } else {
582            DurationOfPart::LimitDuration { duration: s.span }
583        },
584    }
585}
586
587/// ABI-encode a [`TwapStruct`] into raw bytes for on-chain submission.
588///
589/// # Arguments
590///
591/// * `s` - The per-part `TWAP` struct to encode.
592///
593/// # Returns
594///
595/// A 320-byte `Vec<u8>` containing the 10-word ABI-encoded tuple.
596fn encode_struct(s: &TwapStruct) -> Vec<u8> {
597    encode_twap_struct(s)
598}
599
600/// ABI-encode a [`TwapStruct`] into the 320-byte `staticInput` bytes expected by the
601/// on-chain `TWAP` handler.
602///
603/// Encoding layout (each word is 32 bytes, big-endian):
604/// - Words 0-2: `sell_token`, `buy_token`, `receiver` (address, left-padded)
605/// - Words 3-4: `part_sell_amount`, `min_part_limit` (uint256)
606/// - Words 5-8: `t0`, `n`, `t`, `span` (uint32, right-aligned in 32 bytes)
607/// - Word 9: `app_data` (bytes32)
608///
609/// Total: 10 x 32 = 320 bytes.
610///
611/// This is the symmetric counterpart to [`decode_twap_struct`].
612///
613/// ```
614/// use alloy_primitives::{Address, U256};
615/// use cow_composable::{
616///     TwapData, TwapStruct, data_to_struct, decode_twap_struct, encode_twap_struct,
617/// };
618///
619/// let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600)
620///     .with_buy_amount(U256::from(800u64));
621/// let s = data_to_struct(&data).unwrap();
622/// let bytes = encode_twap_struct(&s);
623/// assert_eq!(bytes.len(), 320); // 10 × 32-byte ABI words
624/// let decoded = decode_twap_struct(&bytes).unwrap();
625/// assert_eq!(decoded.n, s.n);
626/// ```
627#[must_use]
628pub fn encode_twap_struct(s: &TwapStruct) -> Vec<u8> {
629    let mut buf = Vec::with_capacity(10 * 32);
630    buf.extend_from_slice(&pad_address(s.sell_token.as_slice()));
631    buf.extend_from_slice(&pad_address(s.buy_token.as_slice()));
632    buf.extend_from_slice(&pad_address(s.receiver.as_slice()));
633    buf.extend_from_slice(&u256_bytes(s.part_sell_amount));
634    buf.extend_from_slice(&u256_bytes(s.min_part_limit));
635    buf.extend_from_slice(&u256_be(u64::from(s.t0)));
636    buf.extend_from_slice(&u256_be(u64::from(s.n)));
637    buf.extend_from_slice(&u256_be(u64::from(s.t)));
638    buf.extend_from_slice(&u256_be(u64::from(s.span)));
639    buf.extend_from_slice(s.app_data.as_slice());
640    buf
641}
642
643/// ABI-decode a 320-byte `staticInput` buffer into [`TwapData`].
644///
645/// Decodes the 10-word ABI tuple produced by [`encode_twap_struct`] and then
646/// converts the per-part [`TwapStruct`] into the user-facing [`TwapData`]
647/// representation via [`struct_to_data`].
648///
649/// # Errors
650///
651/// Returns [`CowError::AppData`] if `bytes` is shorter than 320 bytes.
652pub fn decode_twap_static_input(bytes: &[u8]) -> Result<TwapData, CowError> {
653    Ok(struct_to_data(&decode_twap_struct(bytes)?))
654}
655
656/// ABI-decode a 320-byte `staticInput` buffer into the raw [`TwapStruct`].
657///
658/// Reads 10 consecutive 32-byte ABI words in the order:
659/// `sell_token`, `buy_token`, `receiver`, `part_sell_amount`, `min_part_limit`,
660/// `t0`, `n`, `t`, `span`, `app_data`.
661///
662/// This is the symmetric counterpart to [`encode_twap_struct`].
663///
664/// # Errors
665///
666/// Returns [`CowError::AppData`] if `bytes` is shorter than 320 bytes.
667///
668/// # Example
669///
670/// ```rust
671/// use alloy_primitives::{Address, U256};
672/// use cow_composable::{TwapData, data_to_struct, decode_twap_struct, encode_twap_struct};
673///
674/// let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600)
675///     .with_buy_amount(U256::from(800u64));
676/// let s = data_to_struct(&data).unwrap();
677/// let bytes = encode_twap_struct(&s);
678/// let decoded = decode_twap_struct(&bytes).unwrap();
679/// assert_eq!(decoded.part_sell_amount, s.part_sell_amount);
680/// assert_eq!(decoded.min_part_limit, s.min_part_limit);
681/// assert_eq!(decoded.n, s.n);
682/// assert_eq!(decoded.t, s.t);
683/// ```
684pub fn decode_twap_struct(bytes: &[u8]) -> Result<TwapStruct, CowError> {
685    if bytes.len() < 10 * 32 {
686        return Err(CowError::AppData(format!(
687            "TWAP static input too short: {} bytes (need 320)",
688            bytes.len()
689        )));
690    }
691    let addr = |off: usize| -> Address {
692        let mut a = [0u8; 20];
693        a.copy_from_slice(&bytes[off + 12..off + 32]);
694        Address::new(a)
695    };
696    let u256 = |off: usize| -> U256 { U256::from_be_slice(&bytes[off..off + 32]) };
697    let u32v = |off: usize| -> u32 {
698        u32::from_be_bytes([bytes[off + 28], bytes[off + 29], bytes[off + 30], bytes[off + 31]])
699    };
700
701    let mut app_data = [0u8; 32];
702    app_data.copy_from_slice(&bytes[288..320]);
703
704    Ok(TwapStruct {
705        sell_token: addr(0),
706        buy_token: addr(32),
707        receiver: addr(64),
708        part_sell_amount: u256(96),
709        min_part_limit: u256(128),
710        t0: u32v(160),
711        n: u32v(192),
712        t: u32v(224),
713        span: u32v(256),
714        app_data: B256::new(app_data),
715    })
716}
717
718// ── Helpers ───────────────────────────────────────────────────────────────────
719
720/// Left-pad an address (or shorter slice) to 32 bytes.
721///
722/// # Arguments
723///
724/// * `bytes` - A 20-byte (or shorter) address slice to pad.
725///
726/// # Returns
727///
728/// A 32-byte array with the input right-aligned and zero-filled on the left.
729fn pad_address(bytes: &[u8]) -> [u8; 32] {
730    let mut out = [0u8; 32];
731    out[12..].copy_from_slice(bytes);
732    out
733}
734
735/// Convert a `U256` to its 32-byte big-endian representation.
736///
737/// # Arguments
738///
739/// * `v` - The 256-bit unsigned integer to convert.
740///
741/// # Returns
742///
743/// A 32-byte big-endian byte array.
744const fn u256_bytes(v: U256) -> [u8; 32] {
745    v.to_be_bytes()
746}
747
748/// Encode a `u64` as a 32-byte big-endian ABI word.
749///
750/// # Arguments
751///
752/// * `v` - The `u64` value to encode.
753///
754/// # Returns
755///
756/// A 32-byte array with the value right-aligned in big-endian order and
757/// zero-filled on the left.
758fn u256_be(v: u64) -> [u8; 32] {
759    let mut out = [0u8; 32];
760    out[24..].copy_from_slice(&v.to_be_bytes());
761    out
762}
763
764/// Round `n` up to the next multiple of 32.
765///
766/// # Arguments
767///
768/// * `n` - The byte length to round up.
769///
770/// # Returns
771///
772/// The smallest multiple of 32 that is greater than or equal to `n`.
773const fn pad32_len(n: usize) -> usize {
774    if n.is_multiple_of(32) { n } else { n + (32 - n % 32) }
775}
776
777/// Append `data` to `buf` and zero-pad to a 32-byte boundary.
778///
779/// # Arguments
780///
781/// * `buf` - The output buffer to extend.
782/// * `data` - The raw bytes to append.
783///
784/// # Returns
785///
786/// Nothing; `buf` is extended in place with `data` followed by zero bytes
787/// so that the appended segment is a multiple of 32 bytes long.
788fn pad_into(buf: &mut Vec<u8>, data: &[u8]) {
789    buf.extend_from_slice(data);
790    let rem = data.len() % 32;
791    if rem != 0 {
792        buf.resize(buf.len() + (32 - rem), 0);
793    }
794}
795
796/// Derive a deterministic salt by hashing all TWAP parameters.
797///
798/// # Arguments
799///
800/// * `d` - The `TWAP` order data whose key fields are hashed.
801///
802/// # Returns
803///
804/// A [`B256`] salt computed as
805/// `keccak256(sell_token ++ buy_token ++ sell_amount ++ num_parts)`.
806fn deterministic_salt(d: &TwapData) -> B256 {
807    let mut buf = Vec::with_capacity(20 + 20 + 32 + 4);
808    buf.extend_from_slice(d.sell_token.as_slice());
809    buf.extend_from_slice(d.buy_token.as_slice());
810    buf.extend_from_slice(&u256_bytes(d.sell_amount));
811    buf.extend_from_slice(&d.num_parts.to_be_bytes());
812    keccak256(&buf)
813}
814
815// ── Tests ────────────────────────────────────────────────────────────────────
816
817#[cfg(test)]
818mod tests {
819    use super::*;
820
821    fn sell_token() -> Address {
822        Address::repeat_byte(0x11)
823    }
824
825    fn buy_token() -> Address {
826        Address::repeat_byte(0x22)
827    }
828
829    fn sample_data() -> TwapData {
830        TwapData::sell(sell_token(), buy_token(), U256::from(1000u64), 4, 3600)
831            .with_buy_amount(U256::from(800u64))
832    }
833
834    // ── encode / decode roundtrip ────────────────────────────────────────
835
836    #[test]
837    fn encode_decode_twap_struct_roundtrip() {
838        let data = sample_data();
839        let s = data_to_struct(&data).unwrap();
840        let bytes = encode_twap_struct(&s);
841        assert_eq!(bytes.len(), 320);
842        let decoded = decode_twap_struct(&bytes).unwrap();
843        assert_eq!(decoded.sell_token, s.sell_token);
844        assert_eq!(decoded.buy_token, s.buy_token);
845        assert_eq!(decoded.receiver, s.receiver);
846        assert_eq!(decoded.part_sell_amount, s.part_sell_amount);
847        assert_eq!(decoded.min_part_limit, s.min_part_limit);
848        assert_eq!(decoded.t0, s.t0);
849        assert_eq!(decoded.n, s.n);
850        assert_eq!(decoded.t, s.t);
851        assert_eq!(decoded.span, s.span);
852        assert_eq!(decoded.app_data, s.app_data);
853    }
854
855    #[test]
856    fn data_to_struct_to_data_roundtrip() {
857        let data = sample_data();
858        let s = data_to_struct(&data).unwrap();
859        let back = struct_to_data(&s);
860        assert_eq!(back.sell_token, data.sell_token);
861        assert_eq!(back.buy_token, data.buy_token);
862        assert_eq!(back.sell_amount, data.sell_amount);
863        assert_eq!(back.buy_amount, data.buy_amount);
864        assert_eq!(back.num_parts, data.num_parts);
865        assert_eq!(back.part_duration, data.part_duration);
866    }
867
868    #[test]
869    fn decode_twap_static_input_roundtrip() {
870        let data = sample_data();
871        let s = data_to_struct(&data).unwrap();
872        let bytes = encode_twap_struct(&s);
873        let decoded_data = decode_twap_static_input(&bytes).unwrap();
874        assert_eq!(decoded_data.sell_amount, data.sell_amount);
875        assert_eq!(decoded_data.buy_amount, data.buy_amount);
876    }
877
878    // ── error cases ──────────────────────────────────────────────────────
879
880    #[test]
881    fn decode_twap_struct_too_short() {
882        let result = decode_twap_struct(&[0u8; 319]);
883        assert!(result.is_err());
884    }
885
886    #[test]
887    fn data_to_struct_zero_num_parts() {
888        let mut data = sample_data();
889        data.num_parts = 0;
890        assert!(data_to_struct(&data).is_err());
891    }
892
893    // ── encode_params / decode_params roundtrip ──────────────────────────
894
895    #[test]
896    fn encode_decode_params_roundtrip() {
897        let params = ConditionalOrderParams {
898            handler: Address::repeat_byte(0xaa),
899            salt: B256::new([0xbb; 32]),
900            static_input: vec![0xcc; 50],
901        };
902        let hex = encode_params(&params);
903        let decoded = decode_params(&hex).unwrap();
904        assert_eq!(decoded.handler, params.handler);
905        assert_eq!(decoded.salt, params.salt);
906        assert_eq!(decoded.static_input, params.static_input);
907    }
908
909    #[test]
910    fn decode_params_invalid_hex() {
911        assert!(decode_params("0xZZZZ").is_err());
912    }
913
914    #[test]
915    fn decode_params_too_short() {
916        assert!(decode_params("0xabcd").is_err());
917    }
918
919    // ── order_id ─────────────────────────────────────────────────────────
920
921    #[test]
922    fn order_id_deterministic() {
923        let params = ConditionalOrderParams {
924            handler: Address::ZERO,
925            salt: B256::ZERO,
926            static_input: vec![0xab, 0xcd],
927        };
928        let id1 = order_id(&params);
929        let id2 = order_id(&params);
930        assert_eq!(id1, id2);
931        assert_ne!(id1, B256::ZERO);
932    }
933
934    #[test]
935    fn order_id_changes_with_salt() {
936        let p1 = ConditionalOrderParams {
937            handler: Address::ZERO,
938            salt: B256::ZERO,
939            static_input: vec![],
940        };
941        let p2 = ConditionalOrderParams {
942            handler: Address::ZERO,
943            salt: B256::new([1u8; 32]),
944            static_input: vec![],
945        };
946        assert_ne!(order_id(&p1), order_id(&p2));
947    }
948
949    // ── TwapOrder methods ────────────────────────────────────────────────
950
951    #[test]
952    fn twap_order_new_deterministic_salt() {
953        let data = sample_data();
954        let order1 = TwapOrder::new(data.clone());
955        let order2 = TwapOrder::new(data);
956        assert_eq!(order1.salt, order2.salt);
957    }
958
959    #[test]
960    fn twap_order_with_salt() {
961        let data = sample_data();
962        let salt = B256::new([0xff; 32]);
963        let order = TwapOrder::with_salt(data, salt);
964        assert_eq!(order.salt, salt);
965    }
966
967    #[test]
968    fn twap_order_to_params_and_id() {
969        let order = TwapOrder::new(sample_data());
970        let params = order.to_params().unwrap();
971        assert_eq!(params.handler, TWAP_HANDLER_ADDRESS);
972        let id = order.id().unwrap();
973        assert_ne!(id, B256::ZERO);
974    }
975
976    #[test]
977    fn twap_order_per_part_amounts() {
978        let data = sample_data();
979        let order = TwapOrder::new(data);
980        let (sell, buy) = order.per_part_amounts().unwrap();
981        assert_eq!(sell, U256::from(250u64));
982        assert_eq!(buy, U256::from(200u64));
983    }
984
985    #[test]
986    fn twap_order_per_part_amounts_zero_parts() {
987        let mut data = sample_data();
988        data.num_parts = 0;
989        let order = TwapOrder::new(data);
990        assert!(order.per_part_amounts().is_err());
991    }
992
993    #[test]
994    fn twap_order_is_valid_happy_path() {
995        let order = TwapOrder::new(sample_data());
996        assert!(order.is_valid());
997    }
998
999    #[test]
1000    fn twap_order_is_valid_same_tokens() {
1001        let mut data = sample_data();
1002        data.buy_token = data.sell_token;
1003        let order = TwapOrder::new(data);
1004        assert!(!order.is_valid());
1005    }
1006
1007    #[test]
1008    fn twap_order_is_valid_zero_sell_token() {
1009        let mut data = sample_data();
1010        data.sell_token = Address::ZERO;
1011        assert!(!TwapOrder::new(data).is_valid());
1012    }
1013
1014    #[test]
1015    fn twap_order_is_valid_zero_sell_amount() {
1016        let mut data = sample_data();
1017        data.sell_amount = U256::ZERO;
1018        assert!(!TwapOrder::new(data).is_valid());
1019    }
1020
1021    #[test]
1022    fn twap_order_is_valid_one_part() {
1023        let mut data = sample_data();
1024        data.num_parts = 1;
1025        assert!(!TwapOrder::new(data).is_valid());
1026    }
1027
1028    #[test]
1029    fn twap_order_is_valid_zero_duration() {
1030        let mut data = sample_data();
1031        data.part_duration = 0;
1032        assert!(!TwapOrder::new(data).is_valid());
1033    }
1034
1035    #[test]
1036    fn twap_order_is_valid_sell_amount_not_divisible() {
1037        let mut data = sample_data();
1038        data.sell_amount = U256::from(1001u64); // not divisible by 4
1039        assert!(!TwapOrder::new(data).is_valid());
1040    }
1041
1042    #[test]
1043    fn twap_order_is_valid_limit_duration_exceeds_part_duration() {
1044        let mut data = sample_data();
1045        data.duration_of_part = DurationOfPart::LimitDuration { duration: 7200 }; // > 3600
1046        assert!(!TwapOrder::new(data).is_valid());
1047    }
1048
1049    #[test]
1050    fn twap_order_is_valid_limit_duration_within_bounds() {
1051        let mut data = sample_data();
1052        data.duration_of_part = DurationOfPart::LimitDuration { duration: 1800 };
1053        assert!(TwapOrder::new(data).is_valid());
1054    }
1055
1056    // ── poll_validate ────────────────────────────────────────────────────
1057
1058    #[test]
1059    #[allow(clippy::wildcard_enum_match_arm, reason = "test catch-all for unexpected variants")]
1060    fn poll_validate_at_mining_time_always_success() {
1061        let order = TwapOrder::new(sample_data());
1062        match order.poll_validate(0) {
1063            PollResult::Success { .. } => {}
1064            other => panic!("expected Success, got {other:?}"),
1065        }
1066    }
1067
1068    #[test]
1069    #[allow(clippy::wildcard_enum_match_arm, reason = "test catch-all for unexpected variants")]
1070    fn poll_validate_before_start() {
1071        let mut data = sample_data();
1072        data.start_time = TwapStartTime::At(1_000_000);
1073        let order = TwapOrder::new(data);
1074        match order.poll_validate(999_999) {
1075            PollResult::TryAtEpoch { epoch } => assert_eq!(epoch, 1_000_000),
1076            other => panic!("expected TryAtEpoch, got {other:?}"),
1077        }
1078    }
1079
1080    #[test]
1081    #[allow(clippy::wildcard_enum_match_arm, reason = "test catch-all for unexpected variants")]
1082    fn poll_validate_within_window() {
1083        let mut data = sample_data();
1084        data.start_time = TwapStartTime::At(1_000_000);
1085        let order = TwapOrder::new(data);
1086        // end = 1_000_000 + 4 * 3600 = 1_014_400
1087        match order.poll_validate(1_007_000) {
1088            PollResult::Success { .. } => {}
1089            other => panic!("expected Success, got {other:?}"),
1090        }
1091    }
1092
1093    #[test]
1094    #[allow(clippy::wildcard_enum_match_arm, reason = "test catch-all for unexpected variants")]
1095    fn poll_validate_after_expiry() {
1096        let mut data = sample_data();
1097        data.start_time = TwapStartTime::At(1_000_000);
1098        let order = TwapOrder::new(data);
1099        match order.poll_validate(1_014_400) {
1100            PollResult::DontTryAgain { .. } => {}
1101            other => panic!("expected DontTryAgain, got {other:?}"),
1102        }
1103    }
1104
1105    // ── Accessor methods ─────────────────────────────────────────────────
1106
1107    #[test]
1108    fn twap_order_accessors() {
1109        let mut data = sample_data();
1110        data.start_time = TwapStartTime::At(42);
1111        let order = TwapOrder::new(data);
1112        assert_eq!(order.total_sell_amount(), U256::from(1000u64));
1113        assert_eq!(order.total_buy_amount(), U256::from(800u64));
1114        assert!(order.is_sell());
1115        assert!(!order.is_buy());
1116        assert_eq!(order.start_timestamp(), Some(42));
1117        assert_eq!(order.salt_ref(), &order.salt);
1118        assert_eq!(order.data_ref().num_parts, 4);
1119    }
1120
1121    #[test]
1122    fn twap_order_is_expired_at() {
1123        let mut data = sample_data();
1124        data.start_time = TwapStartTime::At(1_000_000);
1125        let order = TwapOrder::new(data);
1126        assert!(!order.is_expired_at(1_014_399));
1127        assert!(order.is_expired_at(1_014_400));
1128    }
1129
1130    #[test]
1131    fn twap_order_at_mining_time_start_timestamp_none() {
1132        let order = TwapOrder::new(sample_data());
1133        assert_eq!(order.start_timestamp(), None);
1134    }
1135
1136    // ── format_epoch ─────────────────────────────────────────────────────
1137
1138    #[test]
1139    fn format_epoch_known_timestamp() {
1140        let s = format_epoch(1_700_000_000);
1141        assert!(s.starts_with("2023-11-14"));
1142    }
1143
1144    // ── to_struct ────────────────────────────────────────────────────────
1145
1146    #[test]
1147    fn to_struct_with_limit_duration() {
1148        let mut data = sample_data();
1149        data.duration_of_part = DurationOfPart::LimitDuration { duration: 1800 };
1150        let order = TwapOrder::new(data);
1151        let s = order.to_struct().unwrap();
1152        assert_eq!(s.span, 1800);
1153    }
1154
1155    #[test]
1156    fn to_struct_auto_duration() {
1157        let order = TwapOrder::new(sample_data());
1158        let s = order.to_struct().unwrap();
1159        assert_eq!(s.span, 0);
1160    }
1161
1162    // ── struct_to_data edge cases ────────────────────────────────────────
1163
1164    #[test]
1165    fn struct_to_data_at_mining_time() {
1166        let s = data_to_struct(&sample_data()).unwrap();
1167        let data = struct_to_data(&s);
1168        assert!(matches!(data.start_time, TwapStartTime::AtMiningTime));
1169        assert!(matches!(data.duration_of_part, DurationOfPart::Auto));
1170    }
1171
1172    #[test]
1173    fn struct_to_data_with_fixed_start() {
1174        let mut d = sample_data();
1175        d.start_time = TwapStartTime::At(12345);
1176        d.duration_of_part = DurationOfPart::LimitDuration { duration: 600 };
1177        let s = data_to_struct(&d).unwrap();
1178        let back = struct_to_data(&s);
1179        assert!(matches!(back.start_time, TwapStartTime::At(12345)));
1180        assert!(matches!(back.duration_of_part, DurationOfPart::LimitDuration { duration: 600 }));
1181    }
1182
1183    // ── Display ──────────────────────────────────────────────────────────
1184
1185    #[test]
1186    fn twap_order_display_does_not_panic() {
1187        let order = TwapOrder::new(sample_data());
1188        let s = format!("{order}");
1189        assert!(!s.is_empty());
1190    }
1191
1192    #[test]
1193    fn twap_order_is_valid_zero_buy_token() {
1194        let mut data = sample_data();
1195        data.buy_token = Address::ZERO;
1196        assert!(!TwapOrder::new(data).is_valid());
1197    }
1198
1199    #[test]
1200    fn twap_order_is_valid_zero_buy_amount() {
1201        let mut data = sample_data();
1202        data.buy_amount = U256::ZERO;
1203        assert!(!TwapOrder::new(data).is_valid());
1204    }
1205
1206    #[test]
1207    fn twap_order_is_valid_duration_exceeds_max_frequency() {
1208        let mut data = sample_data();
1209        data.part_duration = super::super::types::MAX_FREQUENCY + 1;
1210        assert!(!TwapOrder::new(data).is_valid());
1211    }
1212
1213    #[test]
1214    fn twap_order_is_buy_via_kind() {
1215        let data = TwapData::buy(buy_token(), sell_token(), U256::from(800u64), 4, 3600);
1216        let order = TwapOrder::new(data);
1217        assert!(order.is_buy());
1218        assert!(!order.is_sell());
1219    }
1220
1221    #[test]
1222    fn decode_params_truncated_data() {
1223        // Valid header but data_len claims more bytes than available
1224        let params = ConditionalOrderParams {
1225            handler: Address::ZERO,
1226            salt: B256::ZERO,
1227            static_input: vec![0xaa; 64],
1228        };
1229        let hex = encode_params(&params);
1230        // Truncate the hex to cut off some data bytes
1231        let truncated = &hex[..hex.len() - 10];
1232        assert!(decode_params(truncated).is_err());
1233    }
1234
1235    #[test]
1236    fn format_epoch_invalid_timestamp() {
1237        // A very old negative-like (overflow) timestamp should fallback to raw number
1238        let s = format_epoch(0);
1239        // timestamp 0 = 1970-01-01
1240        assert!(s.contains("1970"));
1241    }
1242}