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    let kind = match kind_index {
239        0 => OrderKind::Sell,
240        1 => OrderKind::Buy,
241        _ => unreachable!(),
242    };
243
244    let partially_fillable = pf_index != 0;
245
246    let sell_token_balance = match sell_index {
247        0 => TokenBalance::Erc20,
248        2 => TokenBalance::External,
249        3 => TokenBalance::Internal,
250        other => {
251            return Err(CowError::Parse {
252                field: "sellTokenBalance",
253                reason: format!("invalid flag index: {other}"),
254            });
255        }
256    };
257
258    let buy_token_balance = match buy_index {
259        0 => TokenBalance::Erc20,
260        1 => TokenBalance::Internal,
261        _ => unreachable!(),
262    };
263
264    Ok(OrderFlags { kind, partially_fillable, sell_token_balance, buy_token_balance })
265}
266
267/// Decode a [`SigningScheme`] from a trade-flags bitfield.
268///
269/// Mirrors `decodeSigningScheme` from the `TypeScript` SDK.
270///
271/// # Arguments
272///
273/// * `bits` — the encoded trade-flags bitfield byte.
274///
275/// # Returns
276///
277/// The decoded [`SigningScheme`] variant.
278///
279/// # Errors
280///
281/// Returns [`CowError::Parse`] if the signing scheme index is invalid.
282///
283/// ```no_run
284/// use cow_types::{
285///     SigningScheme,
286///     flags::{decode_signing_scheme, encode_signing_scheme},
287/// };
288///
289/// let bits = encode_signing_scheme(SigningScheme::Eip1271);
290/// assert_eq!(decode_signing_scheme(bits).unwrap(), SigningScheme::Eip1271);
291/// ```
292pub fn decode_signing_scheme(bits: u8) -> Result<SigningScheme, CowError> {
293    let index = (bits >> SIGNING_SCHEME_OFFSET) & 0x03;
294    match index {
295        0 => Ok(SigningScheme::Eip712),
296        1 => Ok(SigningScheme::EthSign),
297        2 => Ok(SigningScheme::Eip1271),
298        3 => Ok(SigningScheme::PreSign),
299        _ => unreachable!(),
300    }
301}
302
303/// Encode trade flags (order flags + signing scheme) as a single byte bitfield.
304///
305/// Mirrors `encodeTradeFlags` from the `TypeScript` SDK.
306///
307/// # Arguments
308///
309/// * `flags` — the trade flags to encode.
310///
311/// # Returns
312///
313/// A `u8` bitfield combining order flags (bits 0-4) and signing scheme
314/// (bits 5-6).
315///
316/// ```no_run
317/// use cow_types::{
318///     OrderKind, SigningScheme, TokenBalance,
319///     flags::{OrderFlags, TradeFlags, encode_trade_flags},
320/// };
321///
322/// let flags = TradeFlags {
323///     order_flags: OrderFlags {
324///         kind: OrderKind::Sell,
325///         partially_fillable: false,
326///         sell_token_balance: TokenBalance::Erc20,
327///         buy_token_balance: TokenBalance::Erc20,
328///     },
329///     signing_scheme: SigningScheme::Eip712,
330/// };
331/// assert_eq!(encode_trade_flags(&flags), 0);
332/// ```
333#[must_use]
334pub const fn encode_trade_flags(flags: &TradeFlags) -> u8 {
335    encode_order_flags(&flags.order_flags) | encode_signing_scheme(flags.signing_scheme)
336}
337
338/// Decode trade flags from a bitfield.
339///
340/// Mirrors `decodeTradeFlags` from the `TypeScript` SDK.
341///
342/// # Arguments
343///
344/// * `bits` — the encoded trade-flags bitfield byte.
345///
346/// # Returns
347///
348/// The decoded [`TradeFlags`] struct containing order flags and signing scheme.
349///
350/// # Errors
351///
352/// Returns [`CowError::Parse`] if any flag field has an invalid index.
353///
354/// ```no_run
355/// use cow_types::{
356///     OrderKind, SigningScheme, TokenBalance,
357///     flags::{OrderFlags, TradeFlags, decode_trade_flags, encode_trade_flags},
358/// };
359///
360/// let flags = TradeFlags {
361///     order_flags: OrderFlags {
362///         kind: OrderKind::Buy,
363///         partially_fillable: true,
364///         sell_token_balance: TokenBalance::External,
365///         buy_token_balance: TokenBalance::Internal,
366///     },
367///     signing_scheme: SigningScheme::PreSign,
368/// };
369/// let encoded = encode_trade_flags(&flags);
370/// let decoded = decode_trade_flags(encoded).unwrap();
371/// assert_eq!(decoded, flags);
372/// ```
373pub fn decode_trade_flags(bits: u8) -> Result<TradeFlags, CowError> {
374    let order_flags = decode_order_flags(bits)?;
375    let signing_scheme = decode_signing_scheme(bits)?;
376    Ok(TradeFlags { order_flags, signing_scheme })
377}
378
379/// Normalize the buy token balance, converting `External` to `Erc20`.
380///
381/// In the `CoW` Protocol, the `External` balance type only applies to sell
382/// tokens. For buy tokens, `External` is treated as `Erc20`.
383///
384/// Mirrors `normalizeBuyTokenBalance` from the `TypeScript` SDK.
385///
386/// # Arguments
387///
388/// * `balance` — the buy token balance to normalize.
389///
390/// # Returns
391///
392/// The normalized [`TokenBalance`], with [`TokenBalance::External`] mapped to
393/// [`TokenBalance::Erc20`].
394///
395/// ```
396/// use cow_types::{TokenBalance, flags::normalize_buy_token_balance};
397///
398/// assert_eq!(normalize_buy_token_balance(TokenBalance::Erc20), TokenBalance::Erc20);
399/// assert_eq!(normalize_buy_token_balance(TokenBalance::External), TokenBalance::Erc20);
400/// assert_eq!(normalize_buy_token_balance(TokenBalance::Internal), TokenBalance::Internal);
401/// ```
402#[must_use]
403pub const fn normalize_buy_token_balance(balance: TokenBalance) -> TokenBalance {
404    match balance {
405        TokenBalance::Erc20 | TokenBalance::External => TokenBalance::Erc20,
406        TokenBalance::Internal => TokenBalance::Internal,
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn roundtrip_order_flags() {
416        let cases = [
417            OrderFlags {
418                kind: OrderKind::Sell,
419                partially_fillable: false,
420                sell_token_balance: TokenBalance::Erc20,
421                buy_token_balance: TokenBalance::Erc20,
422            },
423            OrderFlags {
424                kind: OrderKind::Buy,
425                partially_fillable: true,
426                sell_token_balance: TokenBalance::External,
427                buy_token_balance: TokenBalance::Internal,
428            },
429            OrderFlags {
430                kind: OrderKind::Sell,
431                partially_fillable: true,
432                sell_token_balance: TokenBalance::Internal,
433                buy_token_balance: TokenBalance::Erc20,
434            },
435        ];
436
437        for flags in &cases {
438            let encoded = encode_order_flags(flags);
439            let decoded = decode_order_flags(encoded).unwrap();
440            assert_eq!(&decoded, flags, "roundtrip failed for {flags:?}");
441        }
442    }
443
444    #[test]
445    fn roundtrip_trade_flags() {
446        let flags = TradeFlags {
447            order_flags: OrderFlags {
448                kind: OrderKind::Buy,
449                partially_fillable: false,
450                sell_token_balance: TokenBalance::Erc20,
451                buy_token_balance: TokenBalance::Internal,
452            },
453            signing_scheme: SigningScheme::Eip1271,
454        };
455        let encoded = encode_trade_flags(&flags);
456        let decoded = decode_trade_flags(encoded).unwrap();
457        assert_eq!(decoded, flags);
458    }
459
460    #[test]
461    fn invalid_sell_token_balance_flag() {
462        // Bit pattern 01 at offset 2 is unused
463        let bits = 0b000_0100;
464        let result = decode_order_flags(bits);
465        assert!(result.is_err());
466    }
467
468    #[test]
469    fn signing_scheme_round_trip() {
470        for scheme in [
471            SigningScheme::Eip712,
472            SigningScheme::EthSign,
473            SigningScheme::Eip1271,
474            SigningScheme::PreSign,
475        ] {
476            let encoded = encode_signing_scheme(scheme);
477            let decoded = decode_signing_scheme(encoded).unwrap();
478            assert_eq!(decoded, scheme);
479        }
480    }
481}