cow_types/flags.rs
1//! Bitfield encoding and decoding of order and trade flags.
2//!
3//! Mirrors `encodeOrderFlags`, `decodeOrderFlags`, `encodeTradeFlags`,
4//! `decodeTradeFlags`, `encodeSigningScheme`, and `decodeSigningScheme`
5//! from the `TypeScript` `contracts-ts` package.
6
7use cow_errors::CowError;
8
9use crate::{OrderKind, SigningScheme, TokenBalance};
10
11/// Order flags extracted from a bitfield.
12///
13/// Corresponds to the `TypeScript` `OrderFlags` type.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct OrderFlags {
16 /// The order kind (sell or buy).
17 pub kind: OrderKind,
18 /// Whether the order is partially fillable.
19 pub partially_fillable: bool,
20 /// Source of sell token balance.
21 pub sell_token_balance: TokenBalance,
22 /// Destination of buy token balance.
23 pub buy_token_balance: TokenBalance,
24}
25
26/// Trade flags: order flags plus a signing scheme.
27///
28/// Corresponds to the `TypeScript` `TradeFlags` type.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct TradeFlags {
31 /// The underlying order flags.
32 pub order_flags: OrderFlags,
33 /// The signing scheme used to encode the signature.
34 pub signing_scheme: SigningScheme,
35}
36
37// ── Bit layout ──────────────────────────────────────────────────────────────
38//
39// Bit 0: kind (0 = sell, 1 = buy)
40// Bit 1: partiallyFillable (0 = false, 1 = true)
41// Bits 2-3: sellTokenBalance (0 = erc20, 1 = unused, 2 = external, 3 = internal)
42// Bit 4: buyTokenBalance (0 = erc20, 1 = internal)
43// Bits 5-6: signingScheme (0 = eip712, 1 = ethsign, 2 = eip1271, 3 = presign)
44
45const KIND_OFFSET: u8 = 0;
46const PARTIALLY_FILLABLE_OFFSET: u8 = 1;
47const SELL_TOKEN_BALANCE_OFFSET: u8 = 2;
48const BUY_TOKEN_BALANCE_OFFSET: u8 = 4;
49const SIGNING_SCHEME_OFFSET: u8 = 5;
50
51/// Encode an [`OrderKind`] into its flag bits.
52///
53/// # Arguments
54///
55/// * `kind` — the order kind to encode.
56///
57/// # Returns
58///
59/// A `u8` with bit 0 set to `0` for [`OrderKind::Sell`] or `1` for
60/// [`OrderKind::Buy`].
61#[must_use]
62pub const fn encode_kind(kind: OrderKind) -> u8 {
63 match kind {
64 OrderKind::Sell => 0,
65 OrderKind::Buy => 1,
66 }
67}
68
69/// Encode a `partially_fillable` bool into its flag bits.
70///
71/// # Arguments
72///
73/// * `pf` — `true` if the order is partially fillable.
74///
75/// # Returns
76///
77/// A `u8` with bit 1 set according to `pf`.
78#[must_use]
79pub const fn encode_partially_fillable(pf: bool) -> u8 {
80 (pf as u8) << PARTIALLY_FILLABLE_OFFSET
81}
82
83/// Encode a sell [`TokenBalance`] into its flag bits.
84///
85/// # Arguments
86///
87/// * `balance` — the sell token balance source.
88///
89/// # Returns
90///
91/// A `u8` with bits 2-3 encoding the balance type (`Erc20` = 0, `External` = 2,
92/// `Internal` = 3).
93#[must_use]
94pub const fn encode_sell_token_balance(balance: TokenBalance) -> u8 {
95 let index = match balance {
96 TokenBalance::Erc20 => 0u8,
97 TokenBalance::External => 2,
98 TokenBalance::Internal => 3,
99 };
100 index << SELL_TOKEN_BALANCE_OFFSET
101}
102
103/// Encode a buy [`TokenBalance`] into its flag bits.
104///
105/// Note: `External` is normalized to `Erc20` for buy token balance, matching
106/// the `TypeScript` `normalizeBuyTokenBalance` behavior.
107///
108/// # Arguments
109///
110/// * `balance` — the buy token balance destination.
111///
112/// # Returns
113///
114/// A `u8` with bit 4 encoding the balance type (`Erc20`/`External` = 0,
115/// `Internal` = 1).
116#[must_use]
117pub const fn encode_buy_token_balance(balance: TokenBalance) -> u8 {
118 let index = match balance {
119 TokenBalance::Erc20 | TokenBalance::External => 0u8,
120 TokenBalance::Internal => 1,
121 };
122 index << BUY_TOKEN_BALANCE_OFFSET
123}
124
125/// Encode a [`SigningScheme`] into its flag bits.
126///
127/// Mirrors `encodeSigningScheme` from the `TypeScript` SDK.
128///
129/// # Arguments
130///
131/// * `scheme` — the signing scheme to encode.
132///
133/// # Returns
134///
135/// A `u8` with bits 5-6 encoding the scheme (`Eip712` = 0, `EthSign` = 1,
136/// `Eip1271` = 2, `PreSign` = 3).
137///
138/// ```
139/// use cow_types::{SigningScheme, flags::encode_signing_scheme};
140///
141/// assert_eq!(encode_signing_scheme(SigningScheme::Eip712), 0b00_00000);
142/// assert_eq!(encode_signing_scheme(SigningScheme::EthSign), 0b01_00000);
143/// assert_eq!(encode_signing_scheme(SigningScheme::Eip1271), 0b10_00000);
144/// assert_eq!(encode_signing_scheme(SigningScheme::PreSign), 0b11_00000);
145/// ```
146#[must_use]
147pub const fn encode_signing_scheme(scheme: SigningScheme) -> u8 {
148 let index = match scheme {
149 SigningScheme::Eip712 => 0u8,
150 SigningScheme::EthSign => 1,
151 SigningScheme::Eip1271 => 2,
152 SigningScheme::PreSign => 3,
153 };
154 index << SIGNING_SCHEME_OFFSET
155}
156
157/// Encode order flags as a single byte bitfield.
158///
159/// Mirrors `encodeOrderFlags` from the `TypeScript` SDK.
160///
161/// # Arguments
162///
163/// * `flags` — the order flags to encode.
164///
165/// # Returns
166///
167/// A `u8` bitfield combining the kind, partially-fillable, sell-token-balance,
168/// and buy-token-balance flags.
169///
170/// ```no_run
171/// use cow_types::{
172/// OrderKind, TokenBalance,
173/// flags::{OrderFlags, encode_order_flags},
174/// };
175///
176/// let flags = OrderFlags {
177/// kind: OrderKind::Sell,
178/// partially_fillable: false,
179/// sell_token_balance: TokenBalance::Erc20,
180/// buy_token_balance: TokenBalance::Erc20,
181/// };
182/// assert_eq!(encode_order_flags(&flags), 0);
183///
184/// let flags = OrderFlags {
185/// kind: OrderKind::Buy,
186/// partially_fillable: true,
187/// sell_token_balance: TokenBalance::External,
188/// buy_token_balance: TokenBalance::Internal,
189/// };
190/// assert_eq!(encode_order_flags(&flags), 0b1_1011);
191/// ```
192#[must_use]
193pub const fn encode_order_flags(flags: &OrderFlags) -> u8 {
194 encode_kind(flags.kind) |
195 encode_partially_fillable(flags.partially_fillable) |
196 encode_sell_token_balance(flags.sell_token_balance) |
197 encode_buy_token_balance(flags.buy_token_balance)
198}
199
200/// Decode order flags from a bitfield.
201///
202/// Mirrors `decodeOrderFlags` from the `TypeScript` SDK.
203///
204/// # Arguments
205///
206/// * `bits` — the encoded bitfield byte.
207///
208/// # Returns
209///
210/// The decoded [`OrderFlags`] struct.
211///
212/// # Errors
213///
214/// Returns [`CowError::Parse`] if any flag field has an invalid index.
215///
216/// ```no_run
217/// use cow_types::{
218/// OrderKind, TokenBalance,
219/// flags::{OrderFlags, decode_order_flags, encode_order_flags},
220/// };
221///
222/// let flags = OrderFlags {
223/// kind: OrderKind::Buy,
224/// partially_fillable: true,
225/// sell_token_balance: TokenBalance::Internal,
226/// buy_token_balance: TokenBalance::Internal,
227/// };
228/// let encoded = encode_order_flags(&flags);
229/// let decoded = decode_order_flags(encoded).unwrap();
230/// assert_eq!(decoded, flags);
231/// ```
232pub fn decode_order_flags(bits: u8) -> Result<OrderFlags, CowError> {
233 let kind_index = (bits >> KIND_OFFSET) & 0x01;
234 let pf_index = (bits >> PARTIALLY_FILLABLE_OFFSET) & 0x01;
235 let sell_index = (bits >> SELL_TOKEN_BALANCE_OFFSET) & 0x03;
236 let buy_index = (bits >> BUY_TOKEN_BALANCE_OFFSET) & 0x01;
237
238 // `kind_index` is `bits & 0x01` so only `0` or `1` are reachable.
239 let kind = if kind_index == 0 { OrderKind::Sell } else { OrderKind::Buy };
240
241 let partially_fillable = pf_index != 0;
242
243 let sell_token_balance = match sell_index {
244 0 => TokenBalance::Erc20,
245 2 => TokenBalance::External,
246 3 => TokenBalance::Internal,
247 other => {
248 return Err(CowError::Parse {
249 field: "sellTokenBalance",
250 reason: format!("invalid flag index: {other}"),
251 });
252 }
253 };
254
255 // `buy_index` is `bits & 0x01` so only `0` or `1` are reachable.
256 let buy_token_balance =
257 if buy_index == 0 { TokenBalance::Erc20 } else { TokenBalance::Internal };
258
259 Ok(OrderFlags { kind, partially_fillable, sell_token_balance, buy_token_balance })
260}
261
262/// Decode a [`SigningScheme`] from a trade-flags bitfield.
263///
264/// Mirrors `decodeSigningScheme` from the `TypeScript` SDK.
265///
266/// # Arguments
267///
268/// * `bits` — the encoded trade-flags bitfield byte.
269///
270/// # Returns
271///
272/// The decoded [`SigningScheme`] variant.
273///
274/// # Errors
275///
276/// Returns [`CowError::Parse`] if the signing scheme index is invalid.
277///
278/// ```no_run
279/// use cow_types::{
280/// SigningScheme,
281/// flags::{decode_signing_scheme, encode_signing_scheme},
282/// };
283///
284/// let bits = encode_signing_scheme(SigningScheme::Eip1271);
285/// assert_eq!(decode_signing_scheme(bits).unwrap(), SigningScheme::Eip1271);
286/// ```
287pub const fn decode_signing_scheme(bits: u8) -> Result<SigningScheme, CowError> {
288 // `bits & 0x03` is `0..=3`; the four arms below are exhaustive.
289 const SCHEMES: [SigningScheme; 4] = [
290 SigningScheme::Eip712,
291 SigningScheme::EthSign,
292 SigningScheme::Eip1271,
293 SigningScheme::PreSign,
294 ];
295 let index = ((bits >> SIGNING_SCHEME_OFFSET) & 0x03) as usize;
296 Ok(SCHEMES[index])
297}
298
299/// Encode trade flags (order flags + signing scheme) as a single byte bitfield.
300///
301/// Mirrors `encodeTradeFlags` from the `TypeScript` SDK.
302///
303/// # Arguments
304///
305/// * `flags` — the trade flags to encode.
306///
307/// # Returns
308///
309/// A `u8` bitfield combining order flags (bits 0-4) and signing scheme
310/// (bits 5-6).
311///
312/// ```no_run
313/// use cow_types::{
314/// OrderKind, SigningScheme, TokenBalance,
315/// flags::{OrderFlags, TradeFlags, encode_trade_flags},
316/// };
317///
318/// let flags = TradeFlags {
319/// order_flags: OrderFlags {
320/// kind: OrderKind::Sell,
321/// partially_fillable: false,
322/// sell_token_balance: TokenBalance::Erc20,
323/// buy_token_balance: TokenBalance::Erc20,
324/// },
325/// signing_scheme: SigningScheme::Eip712,
326/// };
327/// assert_eq!(encode_trade_flags(&flags), 0);
328/// ```
329#[must_use]
330pub const fn encode_trade_flags(flags: &TradeFlags) -> u8 {
331 encode_order_flags(&flags.order_flags) | encode_signing_scheme(flags.signing_scheme)
332}
333
334/// Decode trade flags from a bitfield.
335///
336/// Mirrors `decodeTradeFlags` from the `TypeScript` SDK.
337///
338/// # Arguments
339///
340/// * `bits` — the encoded trade-flags bitfield byte.
341///
342/// # Returns
343///
344/// The decoded [`TradeFlags`] struct containing order flags and signing scheme.
345///
346/// # Errors
347///
348/// Returns [`CowError::Parse`] if any flag field has an invalid index.
349///
350/// ```no_run
351/// use cow_types::{
352/// OrderKind, SigningScheme, TokenBalance,
353/// flags::{OrderFlags, TradeFlags, decode_trade_flags, encode_trade_flags},
354/// };
355///
356/// let flags = TradeFlags {
357/// order_flags: OrderFlags {
358/// kind: OrderKind::Buy,
359/// partially_fillable: true,
360/// sell_token_balance: TokenBalance::External,
361/// buy_token_balance: TokenBalance::Internal,
362/// },
363/// signing_scheme: SigningScheme::PreSign,
364/// };
365/// let encoded = encode_trade_flags(&flags);
366/// let decoded = decode_trade_flags(encoded).unwrap();
367/// assert_eq!(decoded, flags);
368/// ```
369pub fn decode_trade_flags(bits: u8) -> Result<TradeFlags, CowError> {
370 let order_flags = decode_order_flags(bits)?;
371 let signing_scheme = decode_signing_scheme(bits)?;
372 Ok(TradeFlags { order_flags, signing_scheme })
373}
374
375/// Normalize the buy token balance, converting `External` to `Erc20`.
376///
377/// In the `CoW` Protocol, the `External` balance type only applies to sell
378/// tokens. For buy tokens, `External` is treated as `Erc20`.
379///
380/// Mirrors `normalizeBuyTokenBalance` from the `TypeScript` SDK.
381///
382/// # Arguments
383///
384/// * `balance` — the buy token balance to normalize.
385///
386/// # Returns
387///
388/// The normalized [`TokenBalance`], with [`TokenBalance::External`] mapped to
389/// [`TokenBalance::Erc20`].
390///
391/// ```
392/// use cow_types::{TokenBalance, flags::normalize_buy_token_balance};
393///
394/// assert_eq!(normalize_buy_token_balance(TokenBalance::Erc20), TokenBalance::Erc20);
395/// assert_eq!(normalize_buy_token_balance(TokenBalance::External), TokenBalance::Erc20);
396/// assert_eq!(normalize_buy_token_balance(TokenBalance::Internal), TokenBalance::Internal);
397/// ```
398#[must_use]
399pub const fn normalize_buy_token_balance(balance: TokenBalance) -> TokenBalance {
400 match balance {
401 TokenBalance::Erc20 | TokenBalance::External => TokenBalance::Erc20,
402 TokenBalance::Internal => TokenBalance::Internal,
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn roundtrip_order_flags() {
412 let cases = [
413 OrderFlags {
414 kind: OrderKind::Sell,
415 partially_fillable: false,
416 sell_token_balance: TokenBalance::Erc20,
417 buy_token_balance: TokenBalance::Erc20,
418 },
419 OrderFlags {
420 kind: OrderKind::Buy,
421 partially_fillable: true,
422 sell_token_balance: TokenBalance::External,
423 buy_token_balance: TokenBalance::Internal,
424 },
425 OrderFlags {
426 kind: OrderKind::Sell,
427 partially_fillable: true,
428 sell_token_balance: TokenBalance::Internal,
429 buy_token_balance: TokenBalance::Erc20,
430 },
431 ];
432
433 for flags in &cases {
434 let encoded = encode_order_flags(flags);
435 let decoded = decode_order_flags(encoded).unwrap();
436 assert_eq!(&decoded, flags, "roundtrip failed for {flags:?}");
437 }
438 }
439
440 #[test]
441 fn roundtrip_trade_flags() {
442 let flags = TradeFlags {
443 order_flags: OrderFlags {
444 kind: OrderKind::Buy,
445 partially_fillable: false,
446 sell_token_balance: TokenBalance::Erc20,
447 buy_token_balance: TokenBalance::Internal,
448 },
449 signing_scheme: SigningScheme::Eip1271,
450 };
451 let encoded = encode_trade_flags(&flags);
452 let decoded = decode_trade_flags(encoded).unwrap();
453 assert_eq!(decoded, flags);
454 }
455
456 #[test]
457 fn invalid_sell_token_balance_flag() {
458 // Bit pattern 01 at offset 2 is unused
459 let bits = 0b000_0100;
460 let result = decode_order_flags(bits);
461 assert!(result.is_err());
462 }
463
464 #[test]
465 fn signing_scheme_round_trip() {
466 for scheme in [
467 SigningScheme::Eip712,
468 SigningScheme::EthSign,
469 SigningScheme::Eip1271,
470 SigningScheme::PreSign,
471 ] {
472 let encoded = encode_signing_scheme(scheme);
473 let decoded = decode_signing_scheme(encoded).unwrap();
474 assert_eq!(decoded, scheme);
475 }
476 }
477
478 #[test]
479 fn normalize_buy_token_balance_covers_all_variants() {
480 // External collapses to Erc20 on the buy side.
481 assert_eq!(normalize_buy_token_balance(TokenBalance::Erc20), TokenBalance::Erc20);
482 assert_eq!(normalize_buy_token_balance(TokenBalance::External), TokenBalance::Erc20);
483 // Internal must round-trip unchanged — covers the second match arm.
484 assert_eq!(normalize_buy_token_balance(TokenBalance::Internal), TokenBalance::Internal);
485 }
486}