Skip to main content

cow_settlement_interface/data/
intent.rs

1//! Order intents and their canonical byte representation.
2//!
3//! The intent has two representations:
4//!
5//! - [`OrderIntent`] is the idiomatic Rust representation.
6//! - [`EncodedOrderIntent`] is its canonical byte representation: the only
7//!   thing sent on the wire and also the data encoding used to generate the
8//!   order UID. There, `kind` and the intent's booleans share a single flags
9//!   byte.
10//!
11//! Conversion is asymmetric: [`EncodedOrderIntent`]`::from(OrderIntent)` is
12//! infallible, but decoding raw bytes via [`OrderIntent`]`::try_from` returns
13//! `Result` and rejects a flags byte carrying a bit the encoding doesn't
14//! define.
15
16use core::mem::size_of;
17
18use arrayref::{array_refs, mut_array_refs};
19use derive_more::Deref;
20use solana_hash::Hash;
21use solana_program_error::ProgramError;
22use solana_pubkey::Pubkey;
23
24/// Direction of the trade. The discriminants are the values the `kind` bit of
25/// the encoded flags byte takes.
26#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
27#[repr(u8)]
28pub enum OrderKind {
29    #[default]
30    Sell = 0,
31    Buy = 1,
32}
33
34/// Collection of [`OrderIntent`] fields that can be represented as a single bit.
35#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
36pub struct Flags {
37    /// How the order is authenticated: `true` if the owner creates it
38    /// themselves with a `CreateOrder` instruction they sign; `false` if it's
39    /// authenticated off-chain by an Ed25519 signature, which lets anyone
40    /// holding that signature create the order.
41    pub created_on_chain: bool,
42
43    /// Whether `sell_amount` or `buy_amount` is the exact figure; the
44    /// other side is treated as the limit (minimum to receive for `Sell`,
45    /// maximum to spend for `Buy`).
46    pub kind: OrderKind,
47
48    /// If `true`, the order may be filled across multiple settlements;
49    /// proceeds and consumption scale proportionally with the amount of
50    /// the sell side that's been used. If `false`, a single settlement
51    /// must consume the full sell amount (fill-or-kill).
52    pub partially_fillable: bool,
53}
54
55impl Flags {
56    // The bit each field occupies
57    const CREATED_ON_CHAIN: u8 = 1 << 0;
58    const KIND: u8 = 1 << 1;
59    const PARTIALLY_FILLABLE: u8 = 1 << 2;
60
61    /// Every bit the encoding defines; the others are reserved.
62    const DEFINED: u8 = Self::CREATED_ON_CHAIN | Self::KIND | Self::PARTIALLY_FILLABLE;
63}
64
65impl From<Flags> for [u8; 1] {
66    /// The canonical flags byte. Reserved bits are left clear.
67    fn from(flags: Flags) -> Self {
68        let mut byte = 0;
69        if flags.created_on_chain {
70            byte |= Flags::CREATED_ON_CHAIN;
71        }
72        if flags.kind == OrderKind::Buy {
73            byte |= Flags::KIND;
74        }
75        if flags.partially_fillable {
76            byte |= Flags::PARTIALLY_FILLABLE;
77        }
78        [byte]
79    }
80}
81
82impl TryFrom<[u8; 1]> for Flags {
83    type Error = ProgramError;
84
85    /// Decodes a flags byte, rejecting any reserved bit with
86    /// [`ProgramError::InvalidInstructionData`]. A reserved bit carries no
87    /// meaning to this version of the program, so accepting it would give the
88    /// same flags several encodings, and with them several UIDs.
89    fn try_from(bytes: [u8; 1]) -> Result<Self, Self::Error> {
90        let [byte] = bytes;
91        if byte & !Self::DEFINED != 0 {
92            return Err(ProgramError::InvalidInstructionData);
93        }
94        Ok(Flags {
95            created_on_chain: byte & Self::CREATED_ON_CHAIN != 0,
96            kind: if byte & Self::KIND == 0 {
97                OrderKind::Sell
98            } else {
99                OrderKind::Buy
100            },
101            partially_fillable: byte & Self::PARTIALLY_FILLABLE != 0,
102        })
103    }
104}
105
106/// Canonical order intent. Also the exact bytes hashed (SHA-256) to produce the order UID used in the order PDA's seeds,
107/// and the exact wire format of create_order's `intent` argument. Field order and encoding here are load-bearing: they
108/// must match this program's Rust definition exactly.
109#[derive(Clone, Debug, Eq, PartialEq, Default)]
110pub struct OrderIntent {
111    /// Account authorized to create and invalidate this order and whose
112    /// signature authenticates it. For off-chain orders this is the Ed25519
113    /// signer; for on-chain creation it must be the transaction signer.
114    pub owner: Pubkey,
115
116    /// Token account the sell-side funds are pulled from. Implicitly
117    /// encodes the spender. The settlement state PDA must hold the SPL
118    /// `delegate` on this account for the order to be settleable.
119    /// This token account must be owned by the intent owner. An intent
120    /// that doesn't satisfy this property will be rejected.
121    pub sell_token_account: Pubkey,
122
123    /// Mint of the sell token.
124    pub sell_mint: Pubkey,
125
126    /// Token account that receives the buy-side proceeds. Implicitly
127    /// encodes the recipient.
128    pub buy_token_account: Pubkey,
129
130    /// Mint of the buy token.
131    pub buy_mint: Pubkey,
132
133    /// Amount of the sell token. For `Sell` orders this is the exact
134    /// amount to be sold (subject to `partially_fillable`); for `Buy`
135    /// orders it is the maximum the user is willing to spend.
136    pub sell_amount: u64,
137
138    /// Amount of the buy token. For `Buy` orders this is the exact amount
139    /// to be received (subject to `partially_fillable`); for `Sell`
140    /// orders it is the minimum the user is willing to receive.
141    pub buy_amount: u64,
142
143    /// Unix timestamp after which the order expires.
144    /// The order cannot be executed after expiration.
145    pub valid_to: u32,
146
147    /// The settings the encoding packs bit by bit into a single byte; see
148    /// [`Flags`].
149    pub flags: Flags,
150
151    /// Opaque 32 bytes set by the order creator. Not interpreted by the
152    /// settlement program; used off-chain for metadata such as the
153    /// frontend version, slippage hints, or attribution.
154    pub app_data: [u8; 32],
155}
156
157/// Canonical 213-byte representation of an [`OrderIntent`]. The wire format and
158/// the order UID preimage.
159///
160/// Layout: one character per byte, cell widths proportional to field size,
161/// each divider belongs to the cell on its right. The byte range is
162/// annotated below. Amounts and `valid_to` are little-endian encoded.
163///
164/// ```text
165///                                                                                                                                                                           flags ────┐
166/// ┌───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────┬───────┬───┬┬───────────────────────────────┐
167/// │                               │                               │                               │                               │                               │sell_  │buy_   │val││                               │
168/// │             owner             │      sell_token_account       │           sell_mint           │       buy_token_account       │           buy_mint            │       │       │id_││           app_data            │
169/// │                               │                               │                               │                               │                               │amount │amount │to ││                               │
170/// └───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────┴───────┴───┴┴───────────────────────────────┘
171/// 0                               32                              64                              96                              128                             160     168     176 180                           213
172///                                                                                                                                                                                      181
173/// ```
174///
175#[derive(Clone, Debug, Deref, Eq, PartialEq)]
176pub struct EncodedOrderIntent([u8; Self::SIZE]);
177
178impl EncodedOrderIntent {
179    // Per-field widths, derived from the `OrderIntent` field types.
180    const WIDTH_OWNER: usize = size_of::<Pubkey>();
181    const WIDTH_SELL_TOKEN: usize = size_of::<Pubkey>();
182    const WIDTH_SELL_MINT: usize = size_of::<Pubkey>();
183    const WIDTH_BUY_TOKEN: usize = size_of::<Pubkey>();
184    const WIDTH_BUY_MINT: usize = size_of::<Pubkey>();
185    const WIDTH_SELL_AMOUNT: usize = size_of::<u64>();
186    const WIDTH_BUY_AMOUNT: usize = size_of::<u64>();
187    const WIDTH_VALID_TO: usize = size_of::<u32>();
188    const WIDTH_FLAGS: usize = size_of::<u8>();
189    const WIDTH_APP_DATA: usize = size_of::<[u8; 32]>();
190
191    pub const SIZE: usize = 213;
192
193    /// Canonical hash of the bytes.
194    pub fn hash(&self) -> Hash {
195        hash_bytes(&self.0)
196    }
197
198    /// Decode raw bytes to an [`OrderIntent`] and compute the UID in one shot.
199    /// Returns [`ProgramError::InvalidInstructionData`] for a flags byte that
200    /// doesn't encode correctly; every other byte combination decodes.
201    pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderIntent, Hash), ProgramError> {
202        let intent = OrderIntent::try_from(bytes)?;
203        // The UID is the SHA-256 of the input bytes. Hashing the input
204        // (no re-encode) is correct because encode/decode is a bijection on
205        // inputs that pass validation. Any normalization added to the `From`
206        // or `TryFrom` impls later would break this and the UID would silently
207        // diverge from `OrderIntent::uid()`.
208        let uid = hash_bytes(bytes);
209        Ok((intent, uid))
210    }
211}
212
213pub fn hash_bytes(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Hash {
214    solana_sha256_hasher::hashv(&[bytes.as_slice()])
215}
216
217impl From<&EncodedOrderIntent> for [u8; EncodedOrderIntent::SIZE] {
218    fn from(encoded: &EncodedOrderIntent) -> Self {
219        encoded.0
220    }
221}
222
223impl From<&OrderIntent> for EncodedOrderIntent {
224    fn from(intent: &OrderIntent) -> Self {
225        // `mut_array_refs` checks that `SIZE` is consistent with the sum of
226        // the widths.
227        let mut out = [0u8; Self::SIZE];
228        let (
229            owner,
230            sell_token,
231            sell_mint,
232            buy_token,
233            buy_mint,
234            sell_amount,
235            buy_amount,
236            valid_to,
237            flags,
238            app_data,
239        ) = mut_array_refs![
240            &mut out,
241            EncodedOrderIntent::WIDTH_OWNER,
242            EncodedOrderIntent::WIDTH_SELL_TOKEN,
243            EncodedOrderIntent::WIDTH_SELL_MINT,
244            EncodedOrderIntent::WIDTH_BUY_TOKEN,
245            EncodedOrderIntent::WIDTH_BUY_MINT,
246            EncodedOrderIntent::WIDTH_SELL_AMOUNT,
247            EncodedOrderIntent::WIDTH_BUY_AMOUNT,
248            EncodedOrderIntent::WIDTH_VALID_TO,
249            EncodedOrderIntent::WIDTH_FLAGS,
250            EncodedOrderIntent::WIDTH_APP_DATA
251        ];
252        *owner = intent.owner.to_bytes();
253        *sell_token = intent.sell_token_account.to_bytes();
254        *sell_mint = intent.sell_mint.to_bytes();
255        *buy_token = intent.buy_token_account.to_bytes();
256        *buy_mint = intent.buy_mint.to_bytes();
257        *sell_amount = intent.sell_amount.to_le_bytes();
258        *buy_amount = intent.buy_amount.to_le_bytes();
259        *valid_to = intent.valid_to.to_le_bytes();
260        *flags = intent.flags.into();
261        *app_data = intent.app_data;
262        Self(out)
263    }
264}
265
266impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
267    type Error = ProgramError;
268
269    fn try_from(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Result<Self, Self::Error> {
270        // It's important that the byte representation of an intent is unique.
271        // This function should be injective: there shouldn't be two byte
272        // sequences that decode to the same order intent.
273        // If this were to happen, then the user intent may not be recognized
274        // as valid or it might be possible to replay the same order more
275        // than once.
276
277        let (
278            owner,
279            sell_token,
280            sell_mint,
281            buy_token,
282            buy_mint,
283            sell_amount,
284            buy_amount,
285            valid_to,
286            flags,
287            app_data,
288        ) = array_refs![
289            bytes,
290            EncodedOrderIntent::WIDTH_OWNER,
291            EncodedOrderIntent::WIDTH_SELL_TOKEN,
292            EncodedOrderIntent::WIDTH_SELL_MINT,
293            EncodedOrderIntent::WIDTH_BUY_TOKEN,
294            EncodedOrderIntent::WIDTH_BUY_MINT,
295            EncodedOrderIntent::WIDTH_SELL_AMOUNT,
296            EncodedOrderIntent::WIDTH_BUY_AMOUNT,
297            EncodedOrderIntent::WIDTH_VALID_TO,
298            EncodedOrderIntent::WIDTH_FLAGS,
299            EncodedOrderIntent::WIDTH_APP_DATA
300        ];
301
302        Ok(OrderIntent {
303            owner: Pubkey::new_from_array(*owner),
304            sell_token_account: Pubkey::new_from_array(*sell_token),
305            sell_mint: Pubkey::new_from_array(*sell_mint),
306            buy_token_account: Pubkey::new_from_array(*buy_token),
307            buy_mint: Pubkey::new_from_array(*buy_mint),
308            sell_amount: u64::from_le_bytes(*sell_amount),
309            buy_amount: u64::from_le_bytes(*buy_amount),
310            valid_to: u32::from_le_bytes(*valid_to),
311            flags: Flags::try_from(*flags)?,
312            app_data: *app_data,
313        })
314    }
315}
316
317impl TryFrom<&EncodedOrderIntent> for OrderIntent {
318    type Error = ProgramError;
319
320    fn try_from(encoded: &EncodedOrderIntent) -> Result<Self, Self::Error> {
321        OrderIntent::try_from(&encoded.0)
322    }
323}
324
325impl OrderIntent {
326    /// SHA-256 of the canonical bytes. Doubles as the order UID and the
327    /// middle seed of the order PDA. On SBF this compiles to a single
328    /// `sol_sha256` syscall; off-target it goes through the `sha2` crate.
329    pub fn uid(&self) -> Hash {
330        EncodedOrderIntent::from(self).hash()
331    }
332}
333
334#[cfg(any(test, feature = "test-fixtures"))]
335pub mod fixtures {
336    use proptest::{prelude::*, strategy::Union};
337
338    use super::{Flags, OrderIntent, OrderKind, Pubkey};
339
340    /// Every valid [`OrderKind`].
341    pub const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];
342
343    // Hardcoded but verified in a sanity-check test.
344    pub const FLAGS_OFFSET: usize = 180;
345
346    pub fn sample_intent(flags: Flags) -> OrderIntent {
347        OrderIntent {
348            owner: Pubkey::new_from_array([0x11; 32]),
349            sell_token_account: Pubkey::new_from_array([0x22; 32]),
350            sell_mint: Pubkey::new_from_array([0x33; 32]),
351            buy_token_account: Pubkey::new_from_array([0x44; 32]),
352            buy_mint: Pubkey::new_from_array([0x55; 32]),
353            sell_amount: 0x0123_4567_89ab_cdef,
354            buy_amount: 0xfedc_ba98_7654_3210,
355            valid_to: 0xdead_beef,
356            flags,
357            app_data: [0x66; 32],
358        }
359    }
360
361    /// Any valid [`OrderKind`].
362    pub fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
363        Union::new(ALL_ORDER_KINDS.map(Just))
364    }
365
366    /// Any valid [`Flags`].
367    pub fn arb_flags() -> impl Strategy<Value = Flags> {
368        (any::<bool>(), arb_order_kind(), any::<bool>()).prop_map(
369            |(created_on_chain, kind, partially_fillable)| Flags {
370                created_on_chain,
371                kind,
372                partially_fillable,
373            },
374        )
375    }
376
377    /// Any flags byte the decoder accepts.
378    pub fn arb_flags_byte() -> impl Strategy<Value = u8> {
379        any::<u8>().prop_map(|byte| byte & Flags::DEFINED)
380    }
381
382    /// Any flags byte the decoder rejects.
383    pub fn arb_invalid_flags_byte() -> impl Strategy<Value = u8> {
384        any::<u8>().prop_filter("must have at least one bit that is undefined", |byte| {
385            byte & !Flags::DEFINED > 0
386        })
387    }
388
389    /// Any valid [`OrderIntent`].
390    pub fn arb_order_intent() -> impl Strategy<Value = OrderIntent> {
391        (
392            any::<[u8; 32]>(),
393            any::<[u8; 32]>(),
394            any::<[u8; 32]>(),
395            any::<[u8; 32]>(),
396            any::<[u8; 32]>(),
397            any::<u64>(),
398            any::<u64>(),
399            any::<u32>(),
400            arb_flags(),
401            any::<[u8; 32]>(),
402        )
403            .prop_map(
404                |(
405                    owner,
406                    sell_tok,
407                    sell_mint,
408                    buy_tok,
409                    buy_mint,
410                    sell_amount,
411                    buy_amount,
412                    valid_to,
413                    flags,
414                    app,
415                )| {
416                    OrderIntent {
417                        owner: Pubkey::new_from_array(owner),
418                        sell_token_account: Pubkey::new_from_array(sell_tok),
419                        sell_mint: Pubkey::new_from_array(sell_mint),
420                        buy_token_account: Pubkey::new_from_array(buy_tok),
421                        buy_mint: Pubkey::new_from_array(buy_mint),
422                        sell_amount,
423                        buy_amount,
424                        valid_to,
425                        flags,
426                        app_data: app,
427                    }
428                },
429            )
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use crate::data::intent::fixtures::FLAGS_OFFSET;
436
437    use super::fixtures::sample_intent;
438    use super::*;
439
440    // Every shape an `OrderIntent` can take on its validated axes: the
441    // `created_on_chain` flag bit, the `kind` enum, and the
442    // `partially_fillable` flag bit.
443    fn all_flag_shapes() -> impl Iterator<Item = OrderIntent> {
444        [false, true].into_iter().flat_map(|created_on_chain| {
445            fixtures::ALL_ORDER_KINDS.into_iter().flat_map(move |kind| {
446                [false, true].into_iter().map(move |partially_fillable| {
447                    sample_intent(Flags {
448                        created_on_chain,
449                        kind,
450                        partially_fillable,
451                    })
452                })
453            })
454        })
455    }
456
457    // Pin each width to the size of the `OrderIntent` field it encodes. The
458    // widths summing to `SIZE` is enforced separately, at compile time, by the
459    // `array_refs!` / `mut_array_refs!` invocations in the codec.
460    #[test]
461    fn widths_match_field_sizes() {
462        use core::mem::{size_of, size_of_val};
463
464        // Any `OrderIntent` works: `size_of_val` only consults the field
465        // type, never the data.
466        let intent = sample_intent(Default::default());
467
468        assert_eq!(EncodedOrderIntent::WIDTH_OWNER, size_of_val(&intent.owner));
469        assert_eq!(
470            EncodedOrderIntent::WIDTH_SELL_TOKEN,
471            size_of_val(&intent.sell_token_account)
472        );
473        assert_eq!(
474            EncodedOrderIntent::WIDTH_SELL_MINT,
475            size_of_val(&intent.sell_mint)
476        );
477        assert_eq!(
478            EncodedOrderIntent::WIDTH_BUY_TOKEN,
479            size_of_val(&intent.buy_token_account)
480        );
481        assert_eq!(
482            EncodedOrderIntent::WIDTH_BUY_MINT,
483            size_of_val(&intent.buy_mint)
484        );
485        assert_eq!(
486            EncodedOrderIntent::WIDTH_SELL_AMOUNT,
487            size_of_val(&intent.sell_amount)
488        );
489        assert_eq!(
490            EncodedOrderIntent::WIDTH_BUY_AMOUNT,
491            size_of_val(&intent.buy_amount)
492        );
493        assert_eq!(
494            EncodedOrderIntent::WIDTH_VALID_TO,
495            size_of_val(&intent.valid_to)
496        );
497        assert_eq!(
498            EncodedOrderIntent::WIDTH_FLAGS,
499            // in truth if there was a problem here it would actually cause a compilation error
500            size_of_val::<[u8; 1]>(&Flags::default().into())
501        );
502        assert_eq!(
503            EncodedOrderIntent::WIDTH_APP_DATA,
504            size_of_val(&intent.app_data)
505        );
506
507        assert_eq!(EncodedOrderIntent::SIZE, size_of::<EncodedOrderIntent>());
508    }
509
510    #[test]
511    fn every_flag_owns_a_distinct_bit() {
512        let byte = |flags: Flags| <[u8; 1]>::from(flags)[0];
513        let cleared = Flags {
514            created_on_chain: false,
515            kind: OrderKind::Sell,
516            partially_fillable: false,
517        };
518        assert_eq!(byte(cleared), 0);
519
520        let set_one_by_one = [
521            (
522                Flags::CREATED_ON_CHAIN,
523                Flags {
524                    created_on_chain: true,
525                    ..cleared
526                },
527            ),
528            (
529                Flags::KIND,
530                Flags {
531                    kind: OrderKind::Buy,
532                    ..cleared
533                },
534            ),
535            (
536                Flags::PARTIALLY_FILLABLE,
537                Flags {
538                    partially_fillable: true,
539                    ..cleared
540                },
541            ),
542        ];
543        let mut seen = 0u8;
544        for (bit, flags) in set_one_by_one {
545            assert_eq!(bit.count_ones(), 1, "a flag must occupy a single bit");
546            assert_eq!(seen & bit, 0, "two flags must not share a bit");
547            assert!(
548                bit > seen,
549                "each flag must be more significant than the ones before it"
550            );
551            seen |= bit;
552            assert_eq!(byte(flags), bit);
553        }
554        assert_eq!(seen, Flags::DEFINED);
555    }
556
557    #[test]
558    fn roundtrip_all_kind_and_flag_combinations() {
559        for intent in all_flag_shapes() {
560            let encoded = EncodedOrderIntent::from(&intent);
561            let (decoded, _uid) =
562                EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
563            assert_eq!(decoded, intent);
564        }
565    }
566
567    // Locks the bijection invariant called out in `decode_and_hash`: the
568    // UID computed over the raw input bytes must equal the hash of the
569    // canonical re-encoding. If anything ever normalizes during
570    // encode/decode, this test fails.
571    #[test]
572    fn decode_and_hash_uid_matches_encoded_hash() {
573        for intent in all_flag_shapes() {
574            let encoded = EncodedOrderIntent::from(&intent);
575            let (_intent, uid) =
576                EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
577            assert_eq!(uid, encoded.hash());
578        }
579    }
580
581    #[test]
582    fn decode_accepts_defined_flag_bits_only() {
583        let encoded = EncodedOrderIntent::from(&sample_intent(Default::default()));
584        let mut bytes: [u8; EncodedOrderIntent::SIZE] = *encoded;
585        for flags in u8::MIN..=u8::MAX {
586            bytes[FLAGS_OFFSET] = flags;
587            let decoded = EncodedOrderIntent::decode_and_hash(&bytes);
588            if flags & !Flags::DEFINED != 0 {
589                assert_eq!(
590                    decoded.err(),
591                    Some(ProgramError::InvalidInstructionData),
592                    "flags {flags:#04x} sets a reserved bit and must be rejected",
593                );
594            }
595        }
596    }
597
598    #[test]
599    fn uid_digest_regression() {
600        // Compared as hex: `Hash`'s `Display` and `Debug` are both base58,
601        // which is not how we represent order UIDs elsewhere.
602        fn hex(bytes: &[u8]) -> String {
603            bytes.iter().map(|b| format!("{b:02x}")).collect()
604        }
605        let intent = sample_intent(Flags {
606            created_on_chain: true,
607            kind: OrderKind::Buy,
608            partially_fillable: true,
609        });
610        assert_eq!(
611            hex(intent.uid().as_ref()),
612            "de4096c6c100056f1e4636ea4fafefad40fc1d0b37692fe3ca1e0db3644b86bd",
613        );
614    }
615
616    #[test]
617    fn encoding_regression() {
618        let encoded = EncodedOrderIntent::from(&sample_intent(Flags {
619            created_on_chain: true,
620            kind: OrderKind::Buy,
621            partially_fillable: true,
622        }));
623        let encoding: [u8; EncodedOrderIntent::SIZE] = *encoded;
624        #[rustfmt::skip]
625        let expected: [u8; EncodedOrderIntent::SIZE] = [
626            // owner ([0x11; 32])
627            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
628            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
629            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
630            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
631            // sell_token_account ([0x22; 32])
632            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
633            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
634            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
635            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
636            // sell_mint ([0x33; 32])
637            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
638            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
639            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
640            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
641            // buy_token_account ([0x44; 32])
642            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
643            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
644            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
645            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
646            // buy_mint ([0x55; 32])
647            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
648            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
649            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
650            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
651            // sell_amount (0x0123_4567_89ab_cdef, LE u64)
652            0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
653            // buy_amount (0xfedc_ba98_7654_3210, LE u64)
654            0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
655            // valid_to (0xdead_beef, LE u32)
656            0xef, 0xbe, 0xad, 0xde,
657            // flags (created_on_chain | kind (Buy = 1) | partially_fillable)
658            0b00000111,
659            // app_data ([0x66; 32])
660            0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
661            0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
662            0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
663            0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
664        ];
665        assert_eq!(encoding, expected);
666    }
667
668    // Property-based tests, non-deterministic.
669    mod proptest {
670        use ::proptest::{prelude::*, test_runner::TestCaseError};
671
672        use super::*;
673        use crate::data::intent::fixtures::{
674            arb_flags_byte, arb_invalid_flags_byte, arb_order_intent, FLAGS_OFFSET,
675        };
676
677        proptest! {
678            // For any `OrderIntent`, encoding an intent into an encoded
679            // intent and then decoding it with `decode_and_hash()` returns
680            // the same intent plus a UID that matches the encoded bytes'
681            // hash.
682            #[test]
683            fn intent_roundtrip(intent in arb_order_intent()) {
684                let encoded = EncodedOrderIntent::from(&intent);
685                let (decoded, uid) = EncodedOrderIntent::decode_and_hash(&encoded)
686                    .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
687                prop_assert_eq!(decoded, intent);
688                prop_assert_eq!(uid, encoded.hash());
689            }
690
691            // For any bytes whose flags slot is valid, `decode_and_hash` and
692            // then re-encoding produces back the original bytes.
693            #[test]
694            fn bytes_roundtrip(
695                mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
696                flags in arb_flags_byte(),
697            ) {
698                bytes[FLAGS_OFFSET] = flags;
699                let (intent, _uid) = EncodedOrderIntent::decode_and_hash(&bytes)
700                    .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
701                prop_assert_eq!(*EncodedOrderIntent::from(&intent), bytes);
702            }
703
704            // Symmetric: any bytes whose flags byte carries a reserved bit
705            // return `InvalidInstructionData`.
706            #[test]
707            fn rejects_reserved_flag_bits(
708                mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
709                bad_flags in arb_invalid_flags_byte(),
710            ) {
711                bytes[FLAGS_OFFSET] = bad_flags;
712                prop_assert_eq!(
713                    EncodedOrderIntent::decode_and_hash(&bytes),
714                    Err(ProgramError::InvalidInstructionData),
715                );
716            }
717        }
718    }
719}