Skip to main content

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}