cow_types/unsigned_order.rs
1//! [`UnsignedOrder`] — the canonical `CoW` Protocol order struct before signing.
2//!
3//! This type used to live in `cow-signing::types`, but it is referenced by
4//! both `cow-signing` (for EIP-712 hashing) and `cow-settlement` (for encoder
5//! and trade building). Keeping it in an L2 sibling crate would require a
6//! sibling dependency between the two, so it has been pushed down to L1.
7//!
8//! The former convenience method `UnsignedOrder::hash` (which delegated to
9//! `cow_signing::order_hash`) has been dropped during the move. Call
10//! [`cow_signing::order_hash`](https://docs.rs/cow-signing) directly instead.
11
12use std::fmt;
13
14use alloy_primitives::{Address, B256, U256};
15
16use crate::{OrderKind, TokenBalance};
17
18/// An unsigned `CoW` Protocol order ready to be hashed and signed.
19#[derive(Debug, Clone)]
20pub struct UnsignedOrder {
21 /// Token to sell.
22 pub sell_token: Address,
23 /// Token to buy.
24 pub buy_token: Address,
25 /// Address that receives the bought tokens.
26 pub receiver: Address,
27 /// Amount of `sell_token` to sell (after fee, in atoms).
28 pub sell_amount: U256,
29 /// Minimum amount of `buy_token` to receive (in atoms).
30 pub buy_amount: U256,
31 /// Order expiry as Unix timestamp.
32 pub valid_to: u32,
33 /// App-data hash (`bytes32`).
34 pub app_data: B256,
35 /// Protocol fee included in `sell_amount` (in atoms).
36 pub fee_amount: U256,
37 /// Sell or buy direction.
38 pub kind: OrderKind,
39 /// Whether the order may be partially filled.
40 pub partially_fillable: bool,
41 /// Source of sell funds.
42 pub sell_token_balance: TokenBalance,
43 /// Destination of buy funds.
44 pub buy_token_balance: TokenBalance,
45}
46
47impl UnsignedOrder {
48 /// Construct a **sell** order with defaults: ERC-20 balances, `fee_amount = 0`,
49 /// `app_data = B256::ZERO`, `valid_to = 0`, `receiver = Address::ZERO`.
50 ///
51 /// Use the builder methods to override any field before signing.
52 ///
53 /// # Arguments
54 ///
55 /// * `sell_token` - Address of the token to sell.
56 /// * `buy_token` - Address of the token to buy.
57 /// * `sell_amount` - Amount of `sell_token` to sell (in atoms).
58 /// * `buy_amount` - Minimum amount of `buy_token` to receive (in atoms).
59 ///
60 /// # Returns
61 ///
62 /// A new [`UnsignedOrder`] with [`OrderKind::Sell`] and sensible defaults.
63 #[must_use]
64 pub const fn sell(
65 sell_token: Address,
66 buy_token: Address,
67 sell_amount: U256,
68 buy_amount: U256,
69 ) -> Self {
70 Self {
71 sell_token,
72 buy_token,
73 receiver: Address::ZERO,
74 sell_amount,
75 buy_amount,
76 valid_to: 0,
77 app_data: B256::ZERO,
78 fee_amount: U256::ZERO,
79 kind: OrderKind::Sell,
80 partially_fillable: false,
81 sell_token_balance: TokenBalance::Erc20,
82 buy_token_balance: TokenBalance::Erc20,
83 }
84 }
85
86 /// Construct a **buy** order with defaults: ERC-20 balances, `fee_amount = 0`,
87 /// `app_data = B256::ZERO`, `valid_to = 0`, `receiver = Address::ZERO`.
88 ///
89 /// # Arguments
90 ///
91 /// * `sell_token` - Address of the token to sell.
92 /// * `buy_token` - Address of the token to buy.
93 /// * `sell_amount` - Maximum amount of `sell_token` willing to sell (in atoms).
94 /// * `buy_amount` - Amount of `buy_token` to buy (in atoms).
95 ///
96 /// # Returns
97 ///
98 /// A new [`UnsignedOrder`] with [`OrderKind::Buy`] and sensible defaults.
99 #[must_use]
100 pub const fn buy(
101 sell_token: Address,
102 buy_token: Address,
103 sell_amount: U256,
104 buy_amount: U256,
105 ) -> Self {
106 Self {
107 sell_token,
108 buy_token,
109 receiver: Address::ZERO,
110 sell_amount,
111 buy_amount,
112 valid_to: 0,
113 app_data: B256::ZERO,
114 fee_amount: U256::ZERO,
115 kind: OrderKind::Buy,
116 partially_fillable: false,
117 sell_token_balance: TokenBalance::Erc20,
118 buy_token_balance: TokenBalance::Erc20,
119 }
120 }
121
122 /// Override the receiver address.
123 #[must_use]
124 pub const fn with_receiver(mut self, receiver: Address) -> Self {
125 self.receiver = receiver;
126 self
127 }
128
129 /// Set the order expiry as a Unix timestamp.
130 #[must_use]
131 pub const fn with_valid_to(mut self, valid_to: u32) -> Self {
132 self.valid_to = valid_to;
133 self
134 }
135
136 /// Set the app-data hash.
137 #[must_use]
138 pub const fn with_app_data(mut self, app_data: B256) -> Self {
139 self.app_data = app_data;
140 self
141 }
142
143 /// Override the fee amount (defaults to zero).
144 #[must_use]
145 pub const fn with_fee_amount(mut self, fee_amount: U256) -> Self {
146 self.fee_amount = fee_amount;
147 self
148 }
149
150 /// Allow partial fills.
151 #[must_use]
152 pub const fn with_partially_fillable(mut self) -> Self {
153 self.partially_fillable = true;
154 self
155 }
156
157 /// Override the sell-token balance source.
158 #[must_use]
159 pub const fn with_sell_token_balance(mut self, balance: TokenBalance) -> Self {
160 self.sell_token_balance = balance;
161 self
162 }
163
164 /// Override the buy-token balance destination.
165 #[must_use]
166 pub const fn with_buy_token_balance(mut self, balance: TokenBalance) -> Self {
167 self.buy_token_balance = balance;
168 self
169 }
170
171 /// Returns `true` if the order has expired at the given Unix timestamp.
172 ///
173 /// An order is expired when `timestamp > valid_to`.
174 ///
175 /// ```
176 /// use alloy_primitives::{Address, U256};
177 /// use cow_types::UnsignedOrder;
178 ///
179 /// let order = UnsignedOrder::sell(Address::ZERO, Address::ZERO, U256::ZERO, U256::ZERO)
180 /// .with_valid_to(1_000_000);
181 /// assert!(!order.is_expired(999_999));
182 /// assert!(!order.is_expired(1_000_000)); // valid_to is inclusive
183 /// assert!(order.is_expired(1_000_001));
184 /// ```
185 #[must_use]
186 pub const fn is_expired(&self, timestamp: u64) -> bool {
187 timestamp > self.valid_to as u64
188 }
189
190 /// Returns `true` if this is a sell-direction order.
191 #[must_use]
192 pub const fn is_sell(&self) -> bool {
193 self.kind.is_sell()
194 }
195
196 /// Returns `true` if this is a buy-direction order.
197 #[must_use]
198 pub const fn is_buy(&self) -> bool {
199 self.kind.is_buy()
200 }
201
202 /// Returns `true` if a non-zero receiver address is set.
203 #[must_use]
204 pub fn has_custom_receiver(&self) -> bool {
205 !self.receiver.is_zero()
206 }
207
208 /// Returns `true` if a non-zero app-data hash is attached.
209 #[must_use]
210 pub fn has_app_data(&self) -> bool {
211 !self.app_data.is_zero()
212 }
213
214 /// Returns `true` if the fee amount is non-zero.
215 #[must_use]
216 pub fn has_fee(&self) -> bool {
217 !self.fee_amount.is_zero()
218 }
219
220 /// Returns `true` if this order allows partial fills.
221 #[must_use]
222 pub const fn is_partially_fillable(&self) -> bool {
223 self.partially_fillable
224 }
225
226 /// Returns the total token amount at stake: `sell_amount + buy_amount`.
227 ///
228 /// Uses saturating addition to avoid overflow on extreme values.
229 #[must_use]
230 pub const fn total_amount(&self) -> U256 {
231 self.sell_amount.saturating_add(self.buy_amount)
232 }
233}
234
235impl fmt::Display for UnsignedOrder {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 write!(f, "{} {:#x} → {:#x}", self.kind, self.sell_token, self.buy_token)
238 }
239}