Skip to main content

cow_settlement_interface/data/
order.rs

1//! Order PDA bodies and their canonical byte representation.
2//!
3//! The settlement program stores each authenticated order in a dedicated
4//! program-derived account. That account's data area is laid out here.
5//!
6//! Two types live in this module:
7//!
8//! - [`OrderAccount`] is the idiomatic Rust representation. Every value is
9//!   valid by construction: `cancelled` is a `bool`, `intent` is a fully
10//!   decoded [`OrderIntent`].
11//! - [`EncodedOrderAccount`] is its canonical byte representation, that is,
12//!   the exact bytes written to/read from the PDA.
13//!
14//! Conversion is asymmetric: [`EncodedOrderAccount`]`::from(OrderAccount)`
15//! is infallible; decoding raw bytes via [`OrderAccount`]`::try_from`
16//! returns `Result` and rejects an out-of-range `cancelled` byte or any
17//! intent byte the intent decoder rejects. There is no path that produces
18//! an [`OrderAccount`] whose `cancelled` byte or `intent` slot was not
19//! validated.
20
21use core::mem::size_of;
22
23use arrayref::{array_refs, mut_array_refs};
24use derive_more::Deref;
25use solana_account_view::AccountView;
26use solana_address::Address;
27use solana_hash::Hash;
28use solana_program_error::ProgramError;
29use solana_pubkey::Pubkey;
30
31use crate::data::intent::{self, EncodedOrderIntent, OrderIntent};
32use crate::pda::order::order_pda_signer_seeds;
33use crate::SettlementError;
34
35/// Idiomatic representation of an order PDA's body.
36#[derive(Clone, Debug, Eq, PartialEq, Default)]
37pub struct OrderAccount {
38    /// Canonical bump of the PDA this account lives at. Written at creation
39    /// time so callers don't need to supply it; see [`Self::load_from_pda`].
40    pub bump: u8,
41
42    /// `false` = the order is still active and can be filled; `true` = the
43    /// order has been cancelled by the owner and must not be filled.
44    pub cancelled: bool,
45
46    /// Cumulative amount of the sell token withdrawn for this order
47    /// across settlements. Starts at zero; monotonically increases.
48    pub amount_withdrawn: u64,
49
50    /// Cumulative amount of the buy token received for this order
51    /// across settlements. Starts at zero; monotonically increases.
52    pub amount_received: u64,
53
54    /// Account that funded the rent for this PDA. The rent is refunded
55    /// here when the order is cleared.
56    pub created_by: Pubkey,
57
58    /// The order intent stored in this PDA.
59    pub intent: OrderIntent,
60}
61
62impl OrderAccount {
63    /// Load and decode the order at the given PDA, and confirm the PDA is
64    /// derivable from its own data: both the UID and the bump feeding the
65    /// derivation come from the stored body.
66    pub fn load_from_pda(
67        order_pda: &AccountView,
68        program_id: &Address,
69    ) -> Result<Self, ProgramError> {
70        let (account, uid) = {
71            let data = order_pda.try_borrow()?;
72            let bytes: &[u8; EncodedOrderAccount::SIZE] = (&*data)
73                .try_into()
74                .map_err(|_| ProgramError::InvalidAccountData)?;
75            EncodedOrderAccount::decode_and_hash(bytes)?
76        };
77
78        let expected = Address::create_program_address(
79            &order_pda_signer_seeds(&uid, &[account.bump]),
80            program_id,
81        )
82        .map_err(|_| SettlementError::AccountNotDerivable)?;
83        if &expected != order_pda.address() {
84            return Err(SettlementError::AccountNotDerivable.into());
85        }
86
87        Ok(account)
88    }
89}
90
91/// Canonical 201-byte representation of an [`OrderAccount`]. The bytes
92/// written to/read from the order PDA's data area.
93///
94/// Layout: one character per byte, cell widths proportional to field size,
95/// each divider belongs to the cell on its right. Integers are little-endian
96/// (Anchor/Borsh convention). The intent slot holds a verbatim
97/// [`EncodedOrderIntent`]; see that type's docs for its inner layout.
98///
99/// ```text
100///  ┌───── discriminator
101///  │┌──── bump
102///  ││┌─── cancelled
103///  ┌┬┬┬───────┬───────┬───────────────────────────────┬─────────────────...─────────────────┐
104///  ││││amount_│amount_│                               │                                     │
105///  ││││with-  │re-    │           created_by          │     intent (EncodedOrderIntent)     │
106///  ││││drawn  │ceived │                               │                                     │
107///  └┴┴┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘
108/// 0 1 2 3      11      19                              51                ...               201
109/// ```
110#[derive(Clone, Debug, Deref, Eq, PartialEq)]
111pub struct EncodedOrderAccount([u8; Self::SIZE]);
112
113impl EncodedOrderAccount {
114    // Per-field widths, derived from the `OrderAccount` field types.
115    const W_DISCRIMINATOR: usize = size_of::<u8>();
116    const W_BUMP: usize = size_of::<u8>();
117    const W_CANCELLED: usize = size_of::<bool>();
118    const W_AMOUNT_WITHDRAWN: usize = size_of::<u64>();
119    const W_AMOUNT_RECEIVED: usize = size_of::<u64>();
120    const W_CREATED_BY: usize = size_of::<Pubkey>();
121    const W_INTENT: usize = EncodedOrderIntent::SIZE;
122
123    pub const SIZE: usize = 201;
124
125    /// Single-byte account discriminator. See [`crate::SettlementAccount`].
126    pub const DISCRIMINATOR: u8 = crate::SettlementAccount::OrderAccount.discriminator();
127
128    /// Decode the account body and compute the embedded intent's UID in one
129    /// shot, mirroring [`EncodedOrderIntent::decode_and_hash`]. Decoding
130    /// validates the discriminator and the intent; returns
131    /// [`ProgramError::InvalidAccountData`] on a decode error.
132    pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderAccount, Hash), ProgramError> {
133        let order_account = OrderAccount::try_from(*bytes)?;
134        // The order UID is the hash of the intent's canonical bytes. Decoding
135        // succeeded, so the intent slot already holds those exact bytes: hash
136        // them in place rather than using `intent.uid()` to avoid re-encoding.
137        let (_, raw_intent) = array_refs![
138            bytes,
139            EncodedOrderAccount::SIZE - EncodedOrderAccount::W_INTENT,
140            EncodedOrderAccount::W_INTENT
141        ];
142        let intent_uid = intent::hash_bytes(raw_intent);
143        Ok((order_account, intent_uid))
144    }
145}
146
147/// Writes the canonical [`EncodedOrderAccount`] encoding of the given fields
148/// into `buffer`. `encoded_intent` must be a canonical [`EncodedOrderIntent`]
149/// encoding: validating it is the caller's responsibility.
150pub fn write_account(
151    buffer: &mut [u8; EncodedOrderAccount::SIZE],
152    bump: u8,
153    cancelled: bool,
154    amount_withdrawn: u64,
155    amount_received: u64,
156    created_by: &Pubkey,
157    encoded_intent: &[u8; EncodedOrderIntent::SIZE],
158) {
159    let (
160        discriminator_slot,
161        bump_slot,
162        cancelled_slot,
163        amount_withdrawn_slot,
164        amount_received_slot,
165        created_by_slot,
166        intent_slot,
167    ) = mut_array_refs![
168        buffer,
169        EncodedOrderAccount::W_DISCRIMINATOR,
170        EncodedOrderAccount::W_BUMP,
171        EncodedOrderAccount::W_CANCELLED,
172        EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
173        EncodedOrderAccount::W_AMOUNT_RECEIVED,
174        EncodedOrderAccount::W_CREATED_BY,
175        EncodedOrderAccount::W_INTENT
176    ];
177    *discriminator_slot = [EncodedOrderAccount::DISCRIMINATOR];
178    *bump_slot = [bump];
179    *cancelled_slot = [cancelled as u8];
180    *amount_withdrawn_slot = amount_withdrawn.to_le_bytes();
181    *amount_received_slot = amount_received.to_le_bytes();
182    *created_by_slot = created_by.to_bytes();
183    *intent_slot = *encoded_intent;
184}
185
186impl From<EncodedOrderAccount> for [u8; EncodedOrderAccount::SIZE] {
187    fn from(encoded: EncodedOrderAccount) -> Self {
188        encoded.0
189    }
190}
191
192impl From<OrderAccount> for EncodedOrderAccount {
193    fn from(account: OrderAccount) -> Self {
194        let mut out = [0u8; Self::SIZE];
195        write_account(
196            &mut out,
197            account.bump,
198            account.cancelled,
199            account.amount_withdrawn,
200            account.amount_received,
201            &account.created_by,
202            &EncodedOrderIntent::from(&account.intent),
203        );
204        Self(out)
205    }
206}
207
208impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount {
209    type Error = ProgramError;
210
211    fn try_from(bytes: [u8; EncodedOrderAccount::SIZE]) -> Result<Self, Self::Error> {
212        let (discriminator, bump, cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![
213            &bytes,
214            EncodedOrderAccount::W_DISCRIMINATOR,
215            EncodedOrderAccount::W_BUMP,
216            EncodedOrderAccount::W_CANCELLED,
217            EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
218            EncodedOrderAccount::W_AMOUNT_RECEIVED,
219            EncodedOrderAccount::W_CREATED_BY,
220            EncodedOrderAccount::W_INTENT
221        ];
222
223        if *discriminator != [EncodedOrderAccount::DISCRIMINATOR] {
224            return Err(ProgramError::InvalidAccountData);
225        }
226
227        Ok(OrderAccount {
228            bump: bump[0],
229            cancelled: match cancelled {
230                [0] => false,
231                [1] => true,
232                _ => return Err(ProgramError::InvalidAccountData),
233            },
234            amount_withdrawn: u64::from_le_bytes(*amount_withdrawn),
235            amount_received: u64::from_le_bytes(*amount_received),
236            created_by: Pubkey::new_from_array(*created_by),
237            intent: OrderIntent::try_from(intent).map_err(|_| ProgramError::InvalidAccountData)?,
238        })
239    }
240}
241
242impl TryFrom<&[u8]> for OrderAccount {
243    type Error = ProgramError;
244
245    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
246        let bytes: &[u8; EncodedOrderAccount::SIZE] = bytes
247            .try_into()
248            .map_err(|_| ProgramError::InvalidAccountData)?;
249        OrderAccount::try_from(*bytes)
250    }
251}
252
253impl TryFrom<EncodedOrderAccount> for OrderAccount {
254    type Error = ProgramError;
255
256    fn try_from(encoded: EncodedOrderAccount) -> Result<Self, Self::Error> {
257        OrderAccount::try_from(encoded.0)
258    }
259}
260
261#[cfg(any(test, feature = "test-fixtures"))]
262pub mod fixtures {
263    use proptest::prelude::*;
264
265    use super::{OrderAccount, Pubkey};
266    use crate::data::intent::{
267        fixtures::{arb_order_intent, sample_intent},
268        OrderKind,
269    };
270
271    // Hardcoded but verified in a sanity-check test.
272    pub const DISCRIMINATOR_OFFSET: usize = 0;
273    pub const CANCELLED_OFFSET: usize = 2;
274    pub const INTENT_OFFSET: usize = 51;
275
276    /// Hand-picked example order account wrapping [`sample_intent`].
277    pub fn sample_account(cancelled: bool) -> OrderAccount {
278        OrderAccount {
279            bump: 0xfd,
280            cancelled,
281            amount_withdrawn: 0x0112_2334_4556_6778,
282            amount_received: 0x899a_abbc_cdde_eff0,
283            created_by: Pubkey::new_from_array([0x43; 32]),
284            intent: sample_intent(OrderKind::Sell, false),
285        }
286    }
287
288    /// Any valid [`OrderAccount`].
289    pub fn arb_order_account() -> impl Strategy<Value = OrderAccount> {
290        (
291            any::<u8>(),
292            any::<bool>(),
293            any::<u64>(),
294            any::<u64>(),
295            any::<[u8; 32]>(),
296            arb_order_intent(),
297        )
298            .prop_map(
299                |(bump, cancelled, amount_withdrawn, amount_received, created_by, intent)| {
300                    OrderAccount {
301                        bump,
302                        cancelled,
303                        amount_withdrawn,
304                        amount_received,
305                        created_by: Pubkey::new_from_array(created_by),
306                        intent,
307                    }
308                },
309            )
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use core::mem::size_of;
316
317    use super::fixtures::{sample_account, CANCELLED_OFFSET, DISCRIMINATOR_OFFSET, INTENT_OFFSET};
318    use super::*;
319    use crate::data::intent::{
320        fixtures::{sample_intent, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET},
321        OrderKind,
322    };
323
324    // Pin each width to the size of the `OrderAccount` field it encodes. The
325    // widths summing to `SIZE` is enforced separately, at compile time, by the
326    // `array_refs!` / `mut_array_refs!` invocations in the codec.
327    #[test]
328    fn widths_match_field_sizes() {
329        use core::mem::size_of_val;
330
331        // Any `OrderAccount` works: `size_of_val` only consults the field
332        // type, never the data.
333        let OrderAccount {
334            bump,
335            cancelled,
336            amount_withdrawn,
337            amount_received,
338            created_by,
339            // `OrderAccount` decodes the intent, but the encoded order uses
340            // `EncodedOrderIntent`, not `OrderIntent`.
341            intent: _intent,
342        } = sample_account(false);
343
344        assert_eq!(EncodedOrderAccount::W_BUMP, size_of_val(&bump));
345        assert_eq!(EncodedOrderAccount::W_CANCELLED, size_of_val(&cancelled));
346        assert_eq!(
347            EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
348            size_of_val(&amount_withdrawn)
349        );
350        assert_eq!(
351            EncodedOrderAccount::W_AMOUNT_RECEIVED,
352            size_of_val(&amount_received)
353        );
354        assert_eq!(EncodedOrderAccount::W_CREATED_BY, size_of_val(&created_by));
355
356        assert_eq!(
357            EncodedOrderAccount::W_INTENT,
358            size_of::<EncodedOrderIntent>()
359        );
360    }
361
362    #[test]
363    fn roundtrip_both_cancelled_states() {
364        for cancelled in [false, true] {
365            let account = sample_account(cancelled);
366            let encoded = EncodedOrderAccount::from(account.clone());
367            let decoded = OrderAccount::try_from(encoded).expect("example must decode");
368            assert_eq!(decoded, account);
369        }
370    }
371
372    #[test]
373    fn sanity_check_offsets() {
374        fn first_differing_byte(lhs: &[u8], rhs: &[u8]) -> Option<usize> {
375            lhs.iter().zip(rhs).position(|(l, r)| l != r)
376        }
377
378        let mut sample_account_base = sample_account(false);
379        let base: [u8; EncodedOrderAccount::SIZE] =
380            EncodedOrderAccount::from(sample_account_base.clone()).into();
381        let cancelled: [u8; EncodedOrderAccount::SIZE] =
382            EncodedOrderAccount::from(sample_account(true)).into();
383        assert_eq!(
384            first_differing_byte(&base, &cancelled).expect("should differ in the cancelled byte"),
385            CANCELLED_OFFSET
386        );
387
388        // Differs only in the embedded intent.
389        let encoded_intent: [u8; EncodedOrderIntent::SIZE] =
390            (&EncodedOrderIntent::from(&sample_account_base.intent)).into();
391        // Hack: xoring each byte makes sure all bytes are different.
392        // In general, it isn't guaranteed that the result encodes to a
393        // valid intent, but in this case we know it because the only bytes
394        // that may fail decoding are `kind` and `partially_fillable`, both
395        // of which stay valid if flipped with `^0x01`.
396        let bitwise_different_encoded_intent: [u8; EncodedOrderIntent::SIZE] =
397            encoded_intent.map(|b| b ^ 0x01);
398        sample_account_base.intent =
399            OrderIntent::try_from(&bitwise_different_encoded_intent).expect("hack should work");
400        let changed_intent: [u8; EncodedOrderAccount::SIZE] =
401            EncodedOrderAccount::from(sample_account_base).into();
402        assert_eq!(
403            first_differing_byte(&base, &changed_intent).expect("should differ in the intent slot"),
404            INTENT_OFFSET
405        );
406    }
407
408    #[test]
409    fn decode_rejects_wrong_discriminator() {
410        let mut bytes: [u8; EncodedOrderAccount::SIZE] =
411            EncodedOrderAccount::from(sample_account(false)).into();
412        bytes[0] ^= 0xff;
413        let err = OrderAccount::try_from(bytes).expect_err("wrong discriminator must be rejected");
414        assert_eq!(err, ProgramError::InvalidAccountData);
415    }
416
417    #[test]
418    fn decode_rejects_non_boolean_cancelled() {
419        let mut bytes: [u8; EncodedOrderAccount::SIZE] =
420            EncodedOrderAccount::from(sample_account(false)).into();
421        for bad in 0x02u8..=0xff {
422            bytes[CANCELLED_OFFSET] = bad;
423            let err = OrderAccount::try_from(bytes)
424                .expect_err("non-boolean cancelled byte must be rejected");
425            assert_eq!(err, ProgramError::InvalidAccountData);
426        }
427    }
428
429    #[test]
430    fn decode_propagates_invalid_intent() {
431        let mut bytes: [u8; EncodedOrderAccount::SIZE] =
432            EncodedOrderAccount::from(sample_account(false)).into();
433        // Corrupt the `kind` byte inside the intent slot: the intent
434        // decoder rejects it and the order-account decode surfaces that
435        // failure as `InvalidAccountData`.
436        let kind_offset = INTENT_OFFSET + KIND_OFFSET;
437        bytes[kind_offset] = 0x02;
438        let err = OrderAccount::try_from(bytes)
439            .expect_err("an invalid intent kind byte must propagate as a decode failure");
440        assert_eq!(err, ProgramError::InvalidAccountData);
441    }
442
443    #[test]
444    fn decode_and_hash_catches_errors() {
445        let mut bytes: [u8; EncodedOrderAccount::SIZE] =
446            EncodedOrderAccount::from(sample_account(false)).into();
447        // Corrupt the `cancelled` byte to an out-of-range value so the
448        // underlying `try_from` rejects it.
449        bytes[CANCELLED_OFFSET] = 0xff;
450        let err = EncodedOrderAccount::decode_and_hash(&bytes)
451            .expect_err("decode_and_hash must propagate the try_from error");
452        assert_eq!(err, ProgramError::InvalidAccountData);
453    }
454
455    #[test]
456    fn try_from_exact_length_slice_decodes() {
457        let account = sample_account(false);
458        let bytes: [u8; EncodedOrderAccount::SIZE] =
459            EncodedOrderAccount::from(account.clone()).into();
460
461        let decoded = OrderAccount::try_from(&bytes[..]).expect("exact-length slice must decode");
462        assert_eq!(decoded, account);
463    }
464
465    #[test]
466    fn try_from_wrong_length_slice_is_rejected() {
467        let bytes: [u8; EncodedOrderAccount::SIZE] =
468            EncodedOrderAccount::from(sample_account(false)).into();
469
470        assert_eq!(
471            OrderAccount::try_from(&bytes[..EncodedOrderAccount::SIZE - 1]),
472            Err(ProgramError::InvalidAccountData),
473        );
474
475        let too_long = [bytes.as_slice(), [0].as_slice()].concat();
476        assert_eq!(
477            OrderAccount::try_from(&too_long[..]),
478            Err(ProgramError::InvalidAccountData),
479        );
480    }
481
482    #[test]
483    fn try_from_slice_forwards_decoding_errors() {
484        let mut bytes: [u8; EncodedOrderAccount::SIZE] =
485            EncodedOrderAccount::from(sample_account(false)).into();
486        bytes[CANCELLED_OFFSET] = 0xff;
487        assert_eq!(
488            OrderAccount::try_from(&bytes[..]),
489            Err(ProgramError::InvalidAccountData),
490        );
491    }
492
493    mod load_from_pda {
494        use super::*;
495        use crate::instruction::fixtures::fake_account_with_data;
496        use crate::pda::order::find_order_pda;
497
498        const PROGRAM_ID: Address = Address::new_from_array([9; 32]);
499
500        /// [`sample_account`] carrying its own canonical bump, plus the address
501        /// of the PDA it belongs at.
502        fn canonical_account(cancelled: bool) -> (OrderAccount, Address) {
503            let mut account = sample_account(cancelled);
504            let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
505            account.bump = bump;
506            (account, pda_address)
507        }
508
509        #[test]
510        fn accepts_the_canonical_pda() {
511            let (account, pda_address) = canonical_account(false);
512            let order_pda = fake_account_with_data(
513                pda_address,
514                &EncodedOrderAccount::from(account.clone())[..],
515            );
516
517            let loaded = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
518                .expect("canonical PDA must load");
519            assert_eq!(loaded, account);
520        }
521
522        #[test]
523        fn rejects_a_non_canonical_address() {
524            let (account, _) = canonical_account(false);
525            // An address unrelated to the intent's canonical seeds.
526            let wrong_address = Pubkey::new_from_array([0x42; 32]);
527            let order_pda =
528                fake_account_with_data(wrong_address, &EncodedOrderAccount::from(account)[..]);
529
530            let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
531                .expect_err("a non-canonical address must be rejected");
532            assert_eq!(err, SettlementError::AccountNotDerivable.into());
533        }
534
535        #[test]
536        fn rejects_a_stored_bump_that_does_not_derive_the_address() {
537            let (mut account, pda_address) = canonical_account(false);
538            // Any bump other than the canonical one either derives a
539            // different address or fails to derive one at all (falling on
540            // curve); either way, the PDA can no longer be proven canonical.
541            account.bump = account.bump.wrapping_sub(1);
542            let order_pda =
543                fake_account_with_data(pda_address, &EncodedOrderAccount::from(account)[..]);
544
545            let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
546                .expect_err("a non-canonical bump must be rejected");
547            assert_eq!(err, SettlementError::AccountNotDerivable.into());
548        }
549
550        #[test]
551        fn propagates_decode_errors() {
552            let (account, pda_address) = canonical_account(false);
553            let mut bytes: [u8; EncodedOrderAccount::SIZE] =
554                EncodedOrderAccount::from(account).into();
555            bytes[CANCELLED_OFFSET] = 0xff;
556            let order_pda = fake_account_with_data(pda_address, &bytes);
557
558            let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
559                .expect_err("a corrupt account must fail to decode");
560            assert_eq!(err, ProgramError::InvalidAccountData);
561        }
562    }
563
564    #[test]
565    fn direct_write_account_matches_order_account_decoding() {
566        let bump = 254;
567        let cancelled = true;
568        let amount_withdrawn = 1337;
569        let amount_received = 31337;
570        let intent = sample_intent(OrderKind::Sell, false);
571        let created_by = Pubkey::new_from_array([0x42u8; 32]);
572
573        let mut buffer = [0u8; EncodedOrderAccount::SIZE];
574        write_account(
575            &mut buffer,
576            bump,
577            cancelled,
578            amount_withdrawn,
579            amount_received,
580            &created_by,
581            &<[u8; EncodedOrderIntent::SIZE]>::from(&EncodedOrderIntent::from(&intent)),
582        );
583        let direct = EncodedOrderAccount(buffer);
584        let via_order_account = EncodedOrderAccount::from(OrderAccount {
585            bump,
586            cancelled,
587            amount_withdrawn,
588            amount_received,
589            created_by,
590            intent,
591        });
592
593        assert_eq!(direct, via_order_account);
594    }
595
596    // Property-based tests, non-deterministic.
597    mod proptest {
598        use ::proptest::{prelude::*, test_runner::TestCaseError};
599
600        use super::*;
601        use crate::data::{intent::fixtures::arb_order_kind, order::fixtures::arb_order_account};
602
603        proptest! {
604            // For any `OrderAccount`, encode then decode returns the same
605            // account.
606            #[test]
607            fn account_roundtrip(account in arb_order_account()) {
608                let encoded = EncodedOrderAccount::from(account.clone());
609                let decoded = OrderAccount::try_from(encoded)
610                    .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
611                prop_assert_eq!(decoded, account);
612            }
613
614            // For any bytes whose `cancelled` and embedded intent
615            // discriminants are valid, decode + re-encode produces the
616            // same bytes back.
617            #[test]
618            fn bytes_roundtrip(
619                mut bytes in any::<[u8; EncodedOrderAccount::SIZE]>(),
620                cancelled in any::<bool>(),
621                kind in arb_order_kind(),
622                partially_fillable in any::<bool>(),
623            ) {
624                bytes[DISCRIMINATOR_OFFSET] = EncodedOrderAccount::DISCRIMINATOR;
625                bytes[CANCELLED_OFFSET] = cancelled as u8;
626                bytes[INTENT_OFFSET + KIND_OFFSET] = kind as u8;
627                bytes[INTENT_OFFSET + PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
628                let account = OrderAccount::try_from(bytes)
629                    .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
630                prop_assert_eq!(*EncodedOrderAccount::from(account), bytes);
631            }
632
633            #[test]
634            fn consistent_decode_and_hash(account in arb_order_account()) {
635                let encoded = EncodedOrderAccount::from(account.clone());
636                let (decoded, hash) = EncodedOrderAccount::decode_and_hash(&encoded)
637                    .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
638                prop_assert_eq!(hash, account.intent.uid());
639                prop_assert_eq!(decoded, account);
640            }
641        }
642    }
643}