Skip to main content

cow_settlement_interface/data/
intent.rs

1//! Order intents and their canonical byte representation.
2//!
3//! Two types live here:
4//!
5//! - [`OrderIntent`] is the idiomatic Rust representation. Every value is valid
6//!   by construction: `kind` is an [`OrderKind`] enum, `partially_fillable` is
7//!   a `bool`. Callers pattern-match on it directly.
8//! - [`EncodedOrderIntent`] is its canonical byte representation: the only
9//!   thing sent on the wire and also the data encoding used to generate the
10//!   order UID.
11//!
12//! Conversion is asymmetric: [`EncodedOrderIntent`]`::from(OrderIntent)` is
13//! infallible; decoding raw bytes via [`OrderIntent`]`::try_from` returns
14//! `Result` and rejects out-of-range `kind` or `partially_fillable` bytes up
15//! front. There is no path that produces an `OrderIntent` whose `kind` byte or
16//! `partially_fillable` byte was not validated.
17
18use core::mem::size_of;
19
20use arrayref::{array_refs, mut_array_refs};
21use derive_more::Deref;
22use solana_hash::Hash;
23use solana_program_error::ProgramError;
24use solana_pubkey::Pubkey;
25
26/// Direction of the trade.
27#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
28#[repr(u8)]
29pub enum OrderKind {
30    #[default]
31    Sell = 0,
32    Buy = 1,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq, Default)]
36pub struct OrderIntent {
37    /// Account authorized to create and invalidate this order and whose
38    /// signature authenticates it. For off-chain orders this is the Ed25519
39    /// signer; for on-chain creation it must be the transaction signer.
40    pub owner: Pubkey,
41
42    /// Token account that receives the buy-side proceeds. Implicitly
43    /// encodes both the recipient and the buy token, since SPL token
44    /// accounts are token-specific.
45    pub buy_token_account: Pubkey,
46
47    /// Token account the sell-side funds are pulled from. Implicitly
48    /// encodes both the spender and the sell token. The settlement state
49    /// PDA must hold the SPL `delegate` on this account for the order to
50    /// be settleable.
51    /// This token account must be owned by the intent owner. An intent
52    /// that doesn't satisfy this property will be rejected.
53    pub sell_token_account: Pubkey,
54
55    /// Amount of the sell token. For `Sell` orders this is the exact
56    /// amount to be sold (subject to `partially_fillable`); for `Buy`
57    /// orders it is the maximum the user is willing to spend.
58    pub sell_amount: u64,
59
60    /// Amount of the buy token. For `Buy` orders this is the exact amount
61    /// to be received (subject to `partially_fillable`); for `Sell`
62    /// orders it is the minimum the user is willing to receive.
63    pub buy_amount: u64,
64
65    /// Unix timestamp after which the order expires.
66    /// The order cannot be executed after expiration.
67    pub valid_to: u32,
68
69    /// Whether `sell_amount` or `buy_amount` is the exact figure; the
70    /// other side is treated as the limit (minimum to receive for `Sell`,
71    /// maximum to spend for `Buy`).
72    pub kind: OrderKind,
73
74    /// If `true`, the order may be filled across multiple settlements;
75    /// proceeds and consumption scale proportionally with the amount of
76    /// the sell side that's been used. If `false`, a single settlement
77    /// must consume the full sell amount (fill-or-kill).
78    pub partially_fillable: bool,
79
80    /// Opaque 32 bytes set by the order creator. Not interpreted by the
81    /// settlement program; used off-chain for metadata such as the
82    /// frontend version, slippage hints, or attribution.
83    pub app_data: [u8; 32],
84}
85
86/// Canonical 150-byte representation of an [`OrderIntent`]. The wire format and
87/// the order UID preimage.
88///
89/// Layout: one character per byte, cell widths proportional to field size,
90/// each divider belongs to the cell on its right. The byte range is
91/// annotated below. Amounts and `valid_to` are little-endian encoded.
92///
93/// ```text
94///                                                                                              partially_fillable ─────┐
95///                                                                                                            kind ────┐│
96/// ┌───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────┬───────┬───┬┬┬───────────────────────────────┐
97/// │                               │                               │                               │sell_  │buy_   │val│││                               │
98/// │             owner             │       buy_token_account       │       sell_token_account      │       │       │id_│││            app_data           │
99/// │                               │                               │                               │amount │amount │to │││                               │
100/// └───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────┴───────┴───┴┴┴───────────────────────────────┘
101/// 0                               32                              64                              96      104    112 116 118                            150
102///                                                                                                                     117
103/// ```
104#[derive(Clone, Debug, Deref, Eq, PartialEq)]
105pub struct EncodedOrderIntent([u8; Self::SIZE]);
106
107impl EncodedOrderIntent {
108    // Per-field widths, derived from the `OrderIntent` field types.
109    const WIDTH_OWNER: usize = size_of::<Pubkey>();
110    const WIDTH_BUY_TOKEN: usize = size_of::<Pubkey>();
111    const WIDTH_SELL_TOKEN: usize = size_of::<Pubkey>();
112    const WIDTH_SELL_AMOUNT: usize = size_of::<u64>();
113    const WIDTH_BUY_AMOUNT: usize = size_of::<u64>();
114    const WIDTH_VALID_TO: usize = size_of::<u32>();
115    const WIDTH_KIND: usize = size_of::<OrderKind>();
116    const WIDTH_PARTIALLY_FILLABLE: usize = size_of::<bool>();
117    const WIDTH_APP_DATA: usize = size_of::<[u8; 32]>();
118
119    pub const SIZE: usize = 150;
120
121    /// Canonical hash of the bytes.
122    pub fn hash(&self) -> Hash {
123        hash_bytes(&self.0)
124    }
125
126    /// Decode raw bytes to an [`OrderIntent`] and compute the UID in one shot.
127    /// Returns [`ProgramError::InvalidInstructionData`] for an out-of-range
128    /// `kind` or `partially_fillable` byte; every other byte combination
129    /// decodes.
130    pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderIntent, Hash), ProgramError> {
131        let intent = OrderIntent::try_from(bytes)?;
132        // The UID is the SHA-256 of the input bytes. Hashing the input
133        // (no re-encode) is correct because encode/decode is a bijection on
134        // inputs that pass validation. Any normalization added to the `From`
135        // or `TryFrom` impls later would break this and the UID would silently
136        // diverge from `OrderIntent::uid()`.
137        let uid = hash_bytes(bytes);
138        Ok((intent, uid))
139    }
140}
141
142pub fn hash_bytes(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Hash {
143    solana_sha256_hasher::hashv(&[bytes.as_slice()])
144}
145
146impl From<&EncodedOrderIntent> for [u8; EncodedOrderIntent::SIZE] {
147    fn from(encoded: &EncodedOrderIntent) -> Self {
148        encoded.0
149    }
150}
151
152impl From<&OrderIntent> for EncodedOrderIntent {
153    fn from(intent: &OrderIntent) -> Self {
154        // `mut_array_refs` checks that `SIZE` is consistent with the sum of
155        // the widths.
156        let mut out = [0u8; Self::SIZE];
157        let (
158            owner,
159            buy_token,
160            sell_token,
161            sell_amount,
162            buy_amount,
163            valid_to,
164            kind,
165            partially_fillable,
166            app_data,
167        ) = mut_array_refs![
168            &mut out,
169            EncodedOrderIntent::WIDTH_OWNER,
170            EncodedOrderIntent::WIDTH_BUY_TOKEN,
171            EncodedOrderIntent::WIDTH_SELL_TOKEN,
172            EncodedOrderIntent::WIDTH_SELL_AMOUNT,
173            EncodedOrderIntent::WIDTH_BUY_AMOUNT,
174            EncodedOrderIntent::WIDTH_VALID_TO,
175            EncodedOrderIntent::WIDTH_KIND,
176            EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
177            EncodedOrderIntent::WIDTH_APP_DATA
178        ];
179        *owner = intent.owner.to_bytes();
180        *buy_token = intent.buy_token_account.to_bytes();
181        *sell_token = intent.sell_token_account.to_bytes();
182        *sell_amount = intent.sell_amount.to_le_bytes();
183        *buy_amount = intent.buy_amount.to_le_bytes();
184        *valid_to = intent.valid_to.to_le_bytes();
185        *kind = [intent.kind as u8];
186        *partially_fillable = [intent.partially_fillable as u8];
187        *app_data = intent.app_data;
188        Self(out)
189    }
190}
191
192impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
193    type Error = ProgramError;
194
195    fn try_from(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Result<Self, Self::Error> {
196        // It's important that the byte representation of an intent is unique.
197        // This function should be injective: there shouldn't be two byte
198        // sequences that decode to the same order intent.
199        // If this were to happen, then the user intent may not be recognized
200        // as valid or it might be possible to replay the same order more
201        // than once.
202
203        let (
204            owner,
205            buy_token,
206            sell_token,
207            sell_amount,
208            buy_amount,
209            valid_to,
210            kind,
211            partially_fillable,
212            app_data,
213        ) = array_refs![
214            bytes,
215            EncodedOrderIntent::WIDTH_OWNER,
216            EncodedOrderIntent::WIDTH_BUY_TOKEN,
217            EncodedOrderIntent::WIDTH_SELL_TOKEN,
218            EncodedOrderIntent::WIDTH_SELL_AMOUNT,
219            EncodedOrderIntent::WIDTH_BUY_AMOUNT,
220            EncodedOrderIntent::WIDTH_VALID_TO,
221            EncodedOrderIntent::WIDTH_KIND,
222            EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
223            EncodedOrderIntent::WIDTH_APP_DATA
224        ];
225
226        Ok(OrderIntent {
227            owner: Pubkey::new_from_array(*owner),
228            buy_token_account: Pubkey::new_from_array(*buy_token),
229            sell_token_account: Pubkey::new_from_array(*sell_token),
230            sell_amount: u64::from_le_bytes(*sell_amount),
231            buy_amount: u64::from_le_bytes(*buy_amount),
232            valid_to: u32::from_le_bytes(*valid_to),
233            kind: match kind {
234                [0] => OrderKind::Sell,
235                [1] => OrderKind::Buy,
236                _ => return Err(ProgramError::InvalidInstructionData),
237            },
238            partially_fillable: match partially_fillable {
239                [0] => false,
240                [1] => true,
241                _ => return Err(ProgramError::InvalidInstructionData),
242            },
243            app_data: *app_data,
244        })
245    }
246}
247
248impl TryFrom<&EncodedOrderIntent> for OrderIntent {
249    type Error = ProgramError;
250
251    fn try_from(encoded: &EncodedOrderIntent) -> Result<Self, Self::Error> {
252        OrderIntent::try_from(&encoded.0)
253    }
254}
255
256impl OrderIntent {
257    /// SHA-256 of the canonical bytes. Doubles as the order UID and the
258    /// middle seed of the order PDA. On SBF this compiles to a single
259    /// `sol_sha256` syscall; off-target it goes through the `sha2` crate.
260    pub fn uid(&self) -> Hash {
261        EncodedOrderIntent::from(self).hash()
262    }
263}
264
265#[cfg(any(test, feature = "test-fixtures"))]
266pub mod fixtures {
267    use proptest::{prelude::*, strategy::Union};
268
269    use super::{EncodedOrderIntent, OrderIntent, OrderKind, Pubkey};
270
271    /// Every valid [`OrderKind`].
272    pub const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];
273
274    // Hardcoded but verified in a sanity-check test.
275    pub const KIND_OFFSET: usize = 116;
276    pub const PARTIALLY_FILLABLE_OFFSET: usize = KIND_OFFSET + EncodedOrderIntent::WIDTH_KIND;
277
278    pub fn sample_intent(kind: OrderKind, partially_fillable: bool) -> OrderIntent {
279        OrderIntent {
280            owner: Pubkey::new_from_array([0x11; 32]),
281            buy_token_account: Pubkey::new_from_array([0x22; 32]),
282            sell_token_account: Pubkey::new_from_array([0x33; 32]),
283            sell_amount: 0x0123_4567_89ab_cdef,
284            buy_amount: 0xfedc_ba98_7654_3210,
285            valid_to: 0xdead_beef,
286            kind,
287            partially_fillable,
288            app_data: [0x44; 32],
289        }
290    }
291
292    /// Any valid [`OrderKind`].
293    pub fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
294        Union::new(ALL_ORDER_KINDS.map(Just))
295    }
296
297    /// Any valid [`OrderIntent`].
298    pub fn arb_order_intent() -> impl Strategy<Value = OrderIntent> {
299        (
300            any::<[u8; 32]>(),
301            any::<[u8; 32]>(),
302            any::<[u8; 32]>(),
303            any::<u64>(),
304            any::<u64>(),
305            any::<u32>(),
306            arb_order_kind(),
307            any::<bool>(),
308            any::<[u8; 32]>(),
309        )
310            .prop_map(
311                |(owner, buy_tok, sell_tok, sell_amount, buy_amount, valid_to, kind, pf, app)| {
312                    OrderIntent {
313                        owner: Pubkey::new_from_array(owner),
314                        buy_token_account: Pubkey::new_from_array(buy_tok),
315                        sell_token_account: Pubkey::new_from_array(sell_tok),
316                        sell_amount,
317                        buy_amount,
318                        valid_to,
319                        kind,
320                        partially_fillable: pf,
321                        app_data: app,
322                    }
323                },
324            )
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use hex_literal::hex;
331
332    use super::fixtures::{sample_intent, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET};
333    use super::*;
334
335    // Full Cartesian product of `OrderKind × bool` for tests that need to
336    // exercise every shape an `OrderIntent` can take on these axes.
337    fn all_kind_and_fillable() -> impl Iterator<Item = (OrderKind, bool)> {
338        fixtures::ALL_ORDER_KINDS
339            .into_iter()
340            .flat_map(|kind| core::iter::repeat(kind).zip([false, true]))
341    }
342
343    // Pin each width to the size of the `OrderIntent` field it encodes. The
344    // widths summing to `SIZE` is enforced separately, at compile time, by the
345    // `array_refs!` / `mut_array_refs!` invocations in the codec.
346    #[test]
347    fn widths_match_field_sizes() {
348        use core::mem::{size_of, size_of_val};
349
350        // Any `OrderIntent` works: `size_of_val` only consults the field
351        // type, never the data.
352        let intent = sample_intent(OrderKind::Sell, false);
353
354        assert_eq!(EncodedOrderIntent::WIDTH_OWNER, size_of_val(&intent.owner));
355        assert_eq!(
356            EncodedOrderIntent::WIDTH_BUY_TOKEN,
357            size_of_val(&intent.buy_token_account)
358        );
359        assert_eq!(
360            EncodedOrderIntent::WIDTH_SELL_TOKEN,
361            size_of_val(&intent.sell_token_account)
362        );
363        assert_eq!(
364            EncodedOrderIntent::WIDTH_SELL_AMOUNT,
365            size_of_val(&intent.sell_amount)
366        );
367        assert_eq!(
368            EncodedOrderIntent::WIDTH_BUY_AMOUNT,
369            size_of_val(&intent.buy_amount)
370        );
371        assert_eq!(
372            EncodedOrderIntent::WIDTH_VALID_TO,
373            size_of_val(&intent.valid_to)
374        );
375        assert_eq!(EncodedOrderIntent::WIDTH_KIND, size_of_val(&intent.kind));
376        assert_eq!(
377            EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
378            size_of_val(&intent.partially_fillable)
379        );
380        assert_eq!(
381            EncodedOrderIntent::WIDTH_APP_DATA,
382            size_of_val(&intent.app_data)
383        );
384
385        assert_eq!(EncodedOrderIntent::SIZE, size_of::<EncodedOrderIntent>());
386    }
387
388    #[test]
389    fn roundtrip_all_kind_and_bool_combinations() {
390        for (kind, partially_fillable) in all_kind_and_fillable() {
391            let intent = sample_intent(kind, partially_fillable);
392            let encoded = EncodedOrderIntent::from(&intent);
393            let (decoded, _uid) =
394                EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
395            assert_eq!(decoded, intent);
396        }
397    }
398
399    // Locks the bijection invariant called out in `decode_and_hash`: the
400    // UID computed over the raw input bytes must equal the hash of the
401    // canonical re-encoding. If anything ever normalizes during
402    // encode/decode, this test fails.
403    #[test]
404    fn decode_and_hash_uid_matches_encoded_hash() {
405        for (kind, partially_fillable) in all_kind_and_fillable() {
406            let encoded = EncodedOrderIntent::from(&sample_intent(kind, partially_fillable));
407            let (_intent, uid) =
408                EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
409            assert_eq!(uid, encoded.hash());
410        }
411    }
412
413    #[test]
414    fn sanity_check_offsets() {
415        fn first_differing_byte(lhs: &[u8], rhs: &[u8]) -> Option<usize> {
416            lhs.iter().zip(rhs).position(|(l, r)| l != r)
417        }
418        let sell_false: EncodedOrderIntent = (&sample_intent(OrderKind::Sell, false)).into();
419        let sell_true: EncodedOrderIntent = (&sample_intent(OrderKind::Sell, true)).into();
420        let buy_true: EncodedOrderIntent = (&sample_intent(OrderKind::Buy, true)).into();
421
422        assert_eq!(
423            first_differing_byte(sell_false.as_slice(), sell_true.as_slice())
424                .expect("should have different partially fillable byte"),
425            PARTIALLY_FILLABLE_OFFSET
426        );
427        assert_eq!(
428            first_differing_byte(buy_true.as_slice(), sell_true.as_slice())
429                .expect("should have different kind byte"),
430            KIND_OFFSET
431        );
432    }
433
434    #[test]
435    fn decode_rejects_out_of_range_kind() {
436        let encoded = EncodedOrderIntent::from(&sample_intent(OrderKind::Sell, false));
437        let mut bytes: [u8; EncodedOrderIntent::SIZE] = *encoded;
438        for bad in 0x02u8..=0xff {
439            bytes[KIND_OFFSET] = bad;
440            let err = EncodedOrderIntent::decode_and_hash(&bytes)
441                .expect_err("should reject out of range kind");
442            assert_eq!(err, ProgramError::InvalidInstructionData);
443        }
444    }
445
446    #[test]
447    fn decode_rejects_non_boolean_partially_fillable() {
448        let encoded = EncodedOrderIntent::from(&sample_intent(OrderKind::Sell, false));
449        let mut bytes: [u8; EncodedOrderIntent::SIZE] = *encoded;
450        for bad in 0x02u8..=0xff {
451            bytes[PARTIALLY_FILLABLE_OFFSET] = bad;
452            let err = EncodedOrderIntent::decode_and_hash(&bytes)
453                .expect_err("should reject out of range partially fillable");
454            assert_eq!(err, ProgramError::InvalidInstructionData);
455        }
456    }
457
458    #[test]
459    fn uid_digest_regression() {
460        let intent = sample_intent(OrderKind::Buy, true);
461        let expected = hex!("7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00");
462        assert_eq!(intent.uid(), Hash::from(expected));
463    }
464
465    #[test]
466    fn encoding_regression() {
467        let encoded = EncodedOrderIntent::from(&sample_intent(OrderKind::Buy, true));
468        let encoding: [u8; EncodedOrderIntent::SIZE] = *encoded;
469        #[rustfmt::skip]
470        let expected: [u8; EncodedOrderIntent::SIZE] = [
471            // owner ([0x11; 32])
472            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
473            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
474            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
475            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
476            // buy_token_account ([0x22; 32])
477            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
478            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
479            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
480            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
481            // sell_token_account ([0x33; 32])
482            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
483            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
484            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
485            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
486            // sell_amount (0x0123_4567_89ab_cdef, LE u64)
487            0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
488            // buy_amount (0xfedc_ba98_7654_3210, LE u64)
489            0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
490            // valid_to (0xdead_beef, LE u32)
491            0xef, 0xbe, 0xad, 0xde,
492            // kind (Buy = 1)
493            0x01,
494            // partially_fillable (true = 1)
495            0x01,
496            // app_data ([0x44; 32])
497            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
498            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
499            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
500            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
501        ];
502        assert_eq!(encoding, expected);
503    }
504
505    // Property-based tests, non-deterministic.
506    mod proptest {
507        use ::proptest::{prelude::*, test_runner::TestCaseError};
508
509        use super::*;
510        use crate::data::intent::fixtures::{
511            arb_order_intent, arb_order_kind, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET,
512        };
513
514        // Any byte not decoding to a valid order type.
515        fn arb_bad_order_kind_byte() -> impl Strategy<Value = u8> {
516            2u8..=255
517        }
518
519        // Any byte not decoding to a valid bool.
520        fn arb_bad_bool_byte() -> impl Strategy<Value = u8> {
521            2u8..=255
522        }
523
524        proptest! {
525            // For any `OrderIntent`, encoding an intent into an encoded
526            // intent and then decoding it with `decode_and_hash()` returns
527            // the same intent plus a UID that matches the encoded bytes'
528            // hash.
529            #[test]
530            fn intent_roundtrip(intent in arb_order_intent()) {
531                let encoded = EncodedOrderIntent::from(&intent);
532                let (decoded, uid) = EncodedOrderIntent::decode_and_hash(&encoded)
533                    .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
534                prop_assert_eq!(decoded, intent);
535                prop_assert_eq!(uid, encoded.hash());
536            }
537
538            // For any bytes whose `kind` and `partially_fillable` slots
539            // are valid, `decode_and_hash` and then re-encoding produces
540            // back the original bytes.
541            #[test]
542            fn bytes_roundtrip(
543                mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
544                kind in arb_order_kind(),
545                partially_fillable in any::<bool>(),
546            ) {
547                bytes[KIND_OFFSET] = kind as u8;
548                bytes[PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
549                let (intent, _uid) = EncodedOrderIntent::decode_and_hash(&bytes)
550                    .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
551                prop_assert_eq!(*EncodedOrderIntent::from(&intent), bytes);
552            }
553
554            // For any bytes with an invalid `kind` byte (and a valid
555            // `partially_fillable`), `decode_and_hash` returns
556            // `InvalidInstructionData`.
557            #[test]
558            fn rejects_invalid_kind_byte(
559                mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
560                bad_kind in arb_bad_order_kind_byte(),
561                partially_fillable in any::<bool>(),
562            ) {
563                bytes[KIND_OFFSET] = bad_kind;
564                bytes[PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
565                prop_assert_eq!(
566                    EncodedOrderIntent::decode_and_hash(&bytes),
567                    Err(ProgramError::InvalidInstructionData),
568                );
569            }
570
571            // Symmetric: any bytes with an out-of-range
572            // `partially_fillable` byte (and a valid `kind`) return
573            // `InvalidInstructionData`.
574            #[test]
575            fn rejects_invalid_partially_fillable_byte(
576                mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
577                kind in arb_order_kind(),
578                bad_pf in arb_bad_bool_byte(),
579            ) {
580                bytes[KIND_OFFSET] = kind as u8;
581                bytes[PARTIALLY_FILLABLE_OFFSET] = bad_pf;
582                prop_assert_eq!(
583                    EncodedOrderIntent::decode_and_hash(&bytes),
584                    Err(ProgramError::InvalidInstructionData),
585                );
586            }
587        }
588    }
589}