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. Everything outside
6//!   the settlement program uses it.
7//! - [`EncodedOrderIntent`] is the canonical byte representation: the only
8//!   thing sent on the wire and also the data encoding used to generate the
9//!   order UID. There, `kind` and the intent's booleans share a single flags
10//!   byte.
11//!
12//! Conversion is asymmetric: encoding an [`OrderIntent`] is infallible, but
13//! decoding raw bytes returns `Result` and rejects a flags byte carrying a bit
14//! the encoding doesn't 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/// The address an encoded intent carries as a mint to trade native SOL rather
25/// than a token; see [`Asset::Native`].
26pub const ENCODED_NATIVE_SOL_TRANSFER: Pubkey = solana_system_interface::program::ID;
27
28/// Direction of the trade. The discriminants are the values the `kind` bit of
29/// the encoded flags byte takes.
30#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
31#[repr(u8)]
32pub enum OrderKind {
33    #[default]
34    Sell = 0,
35    Buy = 1,
36}
37
38/// Collection of [`OrderIntent`] fields that can be represented as a single bit.
39#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
40pub struct Flags {
41    /// How the order is authenticated: `true` if the owner creates it
42    /// themselves with a `CreateOrder` instruction they sign; `false` if it's
43    /// authenticated off-chain by an Ed25519 signature, which lets anyone
44    /// holding that signature create the order.
45    pub created_on_chain: bool,
46
47    /// Whether `sell_amount` or `buy_amount` is the exact figure; the
48    /// other side is treated as the limit (minimum to receive for `Sell`,
49    /// maximum to spend for `Buy`).
50    pub kind: OrderKind,
51
52    /// If `true`, the order may be filled across multiple settlements;
53    /// proceeds and consumption scale proportionally with the amount of
54    /// the sell side that's been used. If `false`, a single settlement
55    /// must consume the full sell amount (fill-or-kill).
56    pub partially_fillable: bool,
57}
58
59impl Flags {
60    // The bit each field occupies
61    const CREATED_ON_CHAIN: u8 = 1 << 0;
62    const KIND: u8 = 1 << 1;
63    const PARTIALLY_FILLABLE: u8 = 1 << 2;
64
65    /// Every bit the encoding defines; the others are reserved.
66    const DEFINED: u8 = Self::CREATED_ON_CHAIN | Self::KIND | Self::PARTIALLY_FILLABLE;
67}
68
69impl From<Flags> for [u8; 1] {
70    /// The canonical flags byte. Reserved bits are left clear.
71    fn from(flags: Flags) -> Self {
72        let mut byte = 0;
73        if flags.created_on_chain {
74            byte |= Flags::CREATED_ON_CHAIN;
75        }
76        if flags.kind == OrderKind::Buy {
77            byte |= Flags::KIND;
78        }
79        if flags.partially_fillable {
80            byte |= Flags::PARTIALLY_FILLABLE;
81        }
82        [byte]
83    }
84}
85
86impl TryFrom<[u8; 1]> for Flags {
87    type Error = ProgramError;
88
89    /// Decodes a flags byte, rejecting any reserved bit with
90    /// [`ProgramError::InvalidInstructionData`]. A reserved bit carries no
91    /// meaning to this version of the program, so accepting it would give the
92    /// same flags several encodings, and with them several UIDs.
93    fn try_from(bytes: [u8; 1]) -> Result<Self, Self::Error> {
94        let [byte] = bytes;
95        if byte & !Self::DEFINED != 0 {
96            return Err(ProgramError::InvalidInstructionData);
97        }
98        Ok(Flags {
99            created_on_chain: byte & Self::CREATED_ON_CHAIN != 0,
100            kind: if byte & Self::KIND == 0 {
101                OrderKind::Sell
102            } else {
103                OrderKind::Buy
104            },
105            partially_fillable: byte & Self::PARTIALLY_FILLABLE != 0,
106        })
107    }
108}
109
110/// The representation of the side of a trade involving an actual token.
111/// Tokens from the mint `mint` stored at `token_account`.
112#[cfg_attr(any(test, feature = "test-fixtures"), derive(Default))]
113#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct TokenAsset {
115    pub mint: Pubkey,
116    pub token_account: Pubkey,
117}
118
119/// One side of a trade: what moves, and the account it moves through.
120///
121/// The wire spells a side as a `(mint, account)` pair; this is the same pair
122/// with the one combination that isn't a token account named for what it is.
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub enum Asset {
125    /// Native SOL, moving as lamports on this plain account rather than
126    /// through a token account.
127    Native(Pubkey),
128
129    /// Tokens moved by a token program.
130    TokenProgram(TokenAsset),
131}
132
133/// Native SOL has no mint: it moves as lamports, not through a token program.
134/// The error returned when a [`TokenAsset`] naming native SOL is converted to
135/// an [`Asset`].
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub struct NativeSolHasNoMint;
138
139impl TryFrom<TokenAsset> for Asset {
140    type Error = NativeSolHasNoMint;
141
142    /// Rejects a `token` naming [`ENCODED_NATIVE_SOL_TRANSFER`]: that's native SOL, and
143    /// wrapping it as a token side would encode an asset that decodes back as
144    /// [`Asset::Native`].
145    fn try_from(token: TokenAsset) -> Result<Self, Self::Error> {
146        if Self::is_native_sol(token.mint.as_array()) {
147            return Err(NativeSolHasNoMint);
148        }
149        Ok(Asset::TokenProgram(token))
150    }
151}
152
153impl Asset {
154    /// Whether the mint bytes `mint` name native SOL; see
155    /// [`ENCODED_NATIVE_SOL_TRANSFER`].
156    #[inline]
157    #[must_use]
158    pub fn is_native_sol(mint: &[u8; 32]) -> bool {
159        mint == ENCODED_NATIVE_SOL_TRANSFER.as_array()
160    }
161
162    /// The `(mint, account)` pair as used on the wire: a token becomes a mint
163    /// and token account; native SOL becomes [`ENCODED_NATIVE_SOL_TRANSFER`]
164    /// and the address its lamports move on.
165    pub fn encode(&self) -> (Pubkey, Pubkey) {
166        match self {
167            Asset::Native(account) => (ENCODED_NATIVE_SOL_TRANSFER, *account),
168            Asset::TokenProgram(token) => (token.mint, token.token_account),
169        }
170    }
171
172    /// Decode the `(mint, account)` pair the wire carries into the [`Asset`] it
173    /// denotes, for callers that have a side in that shape rather than a chosen
174    /// variant.
175    pub fn decode(mint: Pubkey, account: Pubkey) -> Self {
176        if Self::is_native_sol(mint.as_array()) {
177            Asset::Native(account)
178        } else {
179            Asset::TokenProgram(TokenAsset {
180                mint,
181                token_account: account,
182            })
183        }
184    }
185}
186
187/// Order intent. Its canonical encoding, [`EncodedOrderIntent`], is the exact wire format of create_order's `intent`
188/// argument and the exact bytes hashed (SHA-256) to produce the order UID used in the order PDA's seeds.
189#[cfg_attr(any(test, feature = "test-fixtures"), derive(Default))]
190#[derive(Clone, Debug, Eq, PartialEq)]
191pub struct OrderIntent {
192    /// Account authorized to create and invalidate this order and whose
193    /// signature authenticates it. For off-chain orders this is the Ed25519
194    /// signer; for on-chain creation it must be the transaction signer.
195    pub owner: Pubkey,
196
197    /// What the order sells, and the token account the funds are pulled from.
198    /// That account implicitly encodes the spender: the settlement state PDA
199    /// must hold the SPL `delegate` on it for the order to be settleable, and
200    /// it must be owned by the intent owner. An intent that doesn't satisfy
201    /// this property will be rejected.
202    pub sell: TokenAsset,
203
204    /// What the order buys, and the account that receives the proceeds. That
205    /// account implicitly encodes the recipient
206    pub buy: Asset,
207
208    /// Amount of the sell token. For `Sell` orders this is the exact
209    /// amount to be sold (subject to `partially_fillable`); for `Buy`
210    /// orders it is the maximum the user is willing to spend.
211    pub sell_amount: u64,
212
213    /// Amount of the buy token. For `Buy` orders this is the exact amount
214    /// to be received (subject to `partially_fillable`); for `Sell`
215    /// orders it is the minimum the user is willing to receive.
216    pub buy_amount: u64,
217
218    /// Unix timestamp after which the order expires.
219    /// The order cannot be executed after expiration.
220    pub valid_to: u32,
221
222    /// The settings the encoding packs bit by bit into a single byte; see
223    /// [`Flags`].
224    pub flags: Flags,
225
226    /// Opaque 32 bytes set by the order creator. Not interpreted by the
227    /// settlement program; used off-chain for metadata such as the
228    /// frontend version, slippage hints, or attribution.
229    pub app_data: [u8; 32],
230}
231
232/// Canonical 213-byte representation of an [`OrderIntent`]. The wire format and
233/// the order UID preimage.
234///
235/// Layout: one character per byte, cell widths proportional to field size,
236/// each divider belongs to the cell on its right. The byte range is
237/// annotated below. Amounts and `valid_to` are little-endian encoded.
238///
239/// ```text
240///                                                                                                                                                                           flags ────┐
241/// ┌───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────┬───────┬───┬┬───────────────────────────────┐
242/// │                               │                               │                               │                               │                               │sell_  │buy_   │val││                               │
243/// │             owner             │      sell_token_account       │           sell_mint           │       buy_token_account       │           buy_mint            │       │       │id_││           app_data            │
244/// │                               │                               │                               │                               │                               │amount │amount │to ││                               │
245/// └───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────┴───────┴───┴┴───────────────────────────────┘
246/// 0                               32                              64                              96                              128                             160     168     176 180                           213
247///                                                                                                                                                                                      181
248/// ```
249///
250#[derive(Clone, Debug, Deref, Eq, PartialEq)]
251pub struct EncodedOrderIntent([u8; Self::SIZE]);
252
253impl EncodedOrderIntent {
254    // Per-field widths, derived from the `OrderIntent` field types.
255    const WIDTH_OWNER: usize = size_of::<Pubkey>();
256    const WIDTH_SELL_TOKEN: usize = size_of::<Pubkey>();
257    const WIDTH_SELL_MINT: usize = size_of::<Pubkey>();
258    const WIDTH_BUY_TOKEN: usize = size_of::<Pubkey>();
259    const WIDTH_BUY_MINT: usize = size_of::<Pubkey>();
260    const WIDTH_SELL_AMOUNT: usize = size_of::<u64>();
261    const WIDTH_BUY_AMOUNT: usize = size_of::<u64>();
262    const WIDTH_VALID_TO: usize = size_of::<u32>();
263    const WIDTH_FLAGS: usize = size_of::<u8>();
264    const WIDTH_APP_DATA: usize = size_of::<[u8; 32]>();
265
266    pub const SIZE: usize = 213;
267
268    /// Canonical hash of the bytes.
269    pub fn hash(&self) -> Hash {
270        hash_bytes(&self.0)
271    }
272}
273
274/// Given a slice of intent bytes, verify that it can encode a valid intent.
275#[must_use = "ignoring the result skips the validation"]
276#[inline]
277pub fn check_bytes(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Result<(), ProgramError> {
278    Flags::try_from(*intent_slots(bytes).flags).map(drop)
279}
280
281pub fn hash_bytes(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Hash {
282    solana_sha256_hasher::hashv(&[bytes.as_slice()])
283}
284
285impl From<&EncodedOrderIntent> for [u8; EncodedOrderIntent::SIZE] {
286    fn from(encoded: &EncodedOrderIntent) -> Self {
287        encoded.0
288    }
289}
290
291/// A borrowed view over an intent's bytes, split into named slots so each
292/// field can be named. The slots hold raw encoded bytes, not decoded values.
293#[doc(hidden)]
294pub struct IntentSlots<'a> {
295    pub owner: &'a [u8; EncodedOrderIntent::WIDTH_OWNER],
296    pub sell_token: &'a [u8; EncodedOrderIntent::WIDTH_SELL_TOKEN],
297    pub sell_mint: &'a [u8; EncodedOrderIntent::WIDTH_SELL_MINT],
298    pub buy_token: &'a [u8; EncodedOrderIntent::WIDTH_BUY_TOKEN],
299    pub buy_mint: &'a [u8; EncodedOrderIntent::WIDTH_BUY_MINT],
300    pub sell_amount: &'a [u8; EncodedOrderIntent::WIDTH_SELL_AMOUNT],
301    pub buy_amount: &'a [u8; EncodedOrderIntent::WIDTH_BUY_AMOUNT],
302    pub valid_to: &'a [u8; EncodedOrderIntent::WIDTH_VALID_TO],
303    pub flags: &'a [u8; EncodedOrderIntent::WIDTH_FLAGS],
304    pub app_data: &'a [u8; EncodedOrderIntent::WIDTH_APP_DATA],
305}
306
307/// Split an intent's bytes into its named slots.
308#[inline]
309#[doc(hidden)]
310pub fn intent_slots(bytes: &[u8; EncodedOrderIntent::SIZE]) -> IntentSlots<'_> {
311    let (
312        owner,
313        sell_token,
314        sell_mint,
315        buy_token,
316        buy_mint,
317        sell_amount,
318        buy_amount,
319        valid_to,
320        flags,
321        app_data,
322    ) = array_refs![
323        bytes,
324        EncodedOrderIntent::WIDTH_OWNER,
325        EncodedOrderIntent::WIDTH_SELL_TOKEN,
326        EncodedOrderIntent::WIDTH_SELL_MINT,
327        EncodedOrderIntent::WIDTH_BUY_TOKEN,
328        EncodedOrderIntent::WIDTH_BUY_MINT,
329        EncodedOrderIntent::WIDTH_SELL_AMOUNT,
330        EncodedOrderIntent::WIDTH_BUY_AMOUNT,
331        EncodedOrderIntent::WIDTH_VALID_TO,
332        EncodedOrderIntent::WIDTH_FLAGS,
333        EncodedOrderIntent::WIDTH_APP_DATA
334    ];
335    IntentSlots {
336        owner,
337        sell_token,
338        sell_mint,
339        buy_token,
340        buy_mint,
341        sell_amount,
342        buy_amount,
343        valid_to,
344        flags,
345        app_data,
346    }
347}
348
349impl From<&OrderIntent> for EncodedOrderIntent {
350    /// Lays each side out the way the wire spells it.
351    fn from(intent: &OrderIntent) -> Self {
352        // `mut_array_refs` checks that `SIZE` is consistent with the sum of
353        // the widths.
354        let mut out = [0u8; Self::SIZE];
355        let (
356            owner,
357            sell_token,
358            sell_mint,
359            buy_token,
360            buy_mint,
361            sell_amount,
362            buy_amount,
363            valid_to,
364            flags,
365            app_data,
366        ) = mut_array_refs![
367            &mut out,
368            EncodedOrderIntent::WIDTH_OWNER,
369            EncodedOrderIntent::WIDTH_SELL_TOKEN,
370            EncodedOrderIntent::WIDTH_SELL_MINT,
371            EncodedOrderIntent::WIDTH_BUY_TOKEN,
372            EncodedOrderIntent::WIDTH_BUY_MINT,
373            EncodedOrderIntent::WIDTH_SELL_AMOUNT,
374            EncodedOrderIntent::WIDTH_BUY_AMOUNT,
375            EncodedOrderIntent::WIDTH_VALID_TO,
376            EncodedOrderIntent::WIDTH_FLAGS,
377            EncodedOrderIntent::WIDTH_APP_DATA
378        ];
379        *owner = intent.owner.to_bytes();
380        *sell_token = intent.sell.token_account.to_bytes();
381        *sell_mint = intent.sell.mint.to_bytes();
382        let (buy_mint_address, buy_account) = intent.buy.encode();
383        *buy_token = buy_account.to_bytes();
384        *buy_mint = buy_mint_address.to_bytes();
385        *sell_amount = intent.sell_amount.to_le_bytes();
386        *buy_amount = intent.buy_amount.to_le_bytes();
387        *valid_to = intent.valid_to.to_le_bytes();
388        *flags = intent.flags.into();
389        *app_data = intent.app_data;
390        Self(out)
391    }
392}
393
394impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
395    type Error = ProgramError;
396
397    /// Decode an intent's canonical bytes. Fails with
398    /// [`ProgramError::InvalidInstructionData`] if the flags byte sets a
399    /// reserved bit; every other byte combination decodes.
400    fn try_from(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Result<Self, Self::Error> {
401        // It's important that the byte representation of an intent is unique.
402        // This function should be injective: there shouldn't be two byte
403        // sequences that decode to the same order intent.
404        // If this were to happen, then the user intent may not be recognized
405        // as valid or it might be possible to replay the same order more
406        // than once.
407        let slots = intent_slots(bytes);
408        Ok(OrderIntent {
409            owner: Pubkey::new_from_array(*slots.owner),
410            sell: TokenAsset {
411                mint: Pubkey::new_from_array(*slots.sell_mint),
412                token_account: Pubkey::new_from_array(*slots.sell_token),
413            },
414            buy: Asset::decode(
415                Pubkey::new_from_array(*slots.buy_mint),
416                Pubkey::new_from_array(*slots.buy_token),
417            ),
418            sell_amount: u64::from_le_bytes(*slots.sell_amount),
419            buy_amount: u64::from_le_bytes(*slots.buy_amount),
420            valid_to: u32::from_le_bytes(*slots.valid_to),
421            flags: Flags::try_from(*slots.flags)?,
422            app_data: *slots.app_data,
423        })
424    }
425}
426
427impl OrderIntent {
428    /// SHA-256 of the canonical bytes. Doubles as the order UID and the
429    /// middle seed of the order PDA.
430    pub fn uid(&self) -> Hash {
431        EncodedOrderIntent::from(self).hash()
432    }
433}
434
435#[cfg(any(test, feature = "test-fixtures"))]
436pub mod fixtures {
437    use proptest::{prelude::*, strategy::Union};
438
439    use super::{Asset, Flags, OrderIntent, OrderKind, Pubkey, TokenAsset};
440
441    /// Every valid [`OrderKind`].
442    pub const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];
443
444    // Hardcoded but verified in a sanity-check test.
445    pub const FLAGS_OFFSET: usize = 180;
446
447    impl Default for Asset {
448        /// Native SOL on the all-zero address, the side an all-zero encoding
449        fn default() -> Self {
450            Asset::Native(Pubkey::default())
451        }
452    }
453
454    pub fn sample_intent(flags: Flags) -> OrderIntent {
455        OrderIntent {
456            owner: Pubkey::new_from_array([0x11; 32]),
457            sell: TokenAsset {
458                token_account: Pubkey::new_from_array([0x22; 32]),
459                mint: Pubkey::new_from_array([0x33; 32]),
460            },
461            buy: Asset::TokenProgram(TokenAsset {
462                token_account: Pubkey::new_from_array([0x44; 32]),
463                mint: Pubkey::new_from_array([0x55; 32]),
464            }),
465            sell_amount: 0x0123_4567_89ab_cdef,
466            buy_amount: 0xfedc_ba98_7654_3210,
467            valid_to: 0xdead_beef,
468            flags,
469            app_data: [0x66; 32],
470        }
471    }
472
473    /// Any valid [`OrderKind`].
474    pub fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
475        Union::new(ALL_ORDER_KINDS.map(Just))
476    }
477
478    /// Any valid [`Flags`].
479    pub fn arb_flags() -> impl Strategy<Value = Flags> {
480        (any::<bool>(), arb_order_kind(), any::<bool>()).prop_map(
481            |(created_on_chain, kind, partially_fillable)| Flags {
482                created_on_chain,
483                kind,
484                partially_fillable,
485            },
486        )
487    }
488
489    /// Any flags byte the decoder accepts.
490    pub fn arb_flags_byte() -> impl Strategy<Value = u8> {
491        any::<u8>().prop_map(|byte| byte & Flags::DEFINED)
492    }
493
494    /// Any flags byte the decoder rejects.
495    pub fn arb_invalid_flags_byte() -> impl Strategy<Value = u8> {
496        any::<u8>().prop_filter("must have at least one bit that is undefined", |byte| {
497            byte & !Flags::DEFINED > 0
498        })
499    }
500
501    /// Any valid [`Asset`], either variant equally likely.
502    pub fn arb_asset() -> impl Strategy<Value = Asset> {
503        prop_oneof![
504            any::<[u8; 32]>().prop_map(|account| Asset::Native(Pubkey::new_from_array(account))),
505            (any::<[u8; 32]>(), any::<[u8; 32]>()).prop_filter_map(
506                "mint must not be the native SOL marker",
507                |(mint, token_account)| Asset::try_from(TokenAsset {
508                    mint: Pubkey::new_from_array(mint),
509                    token_account: Pubkey::new_from_array(token_account),
510                })
511                .ok(),
512            ),
513        ]
514    }
515
516    /// Any valid [`OrderIntent`].
517    pub fn arb_order_intent() -> impl Strategy<Value = OrderIntent> {
518        (
519            any::<[u8; 32]>(),
520            any::<[u8; 32]>(),
521            any::<[u8; 32]>(),
522            arb_asset(),
523            any::<u64>(),
524            any::<u64>(),
525            any::<u32>(),
526            arb_flags(),
527            any::<[u8; 32]>(),
528        )
529            .prop_map(
530                |(
531                    owner,
532                    sell_tok,
533                    sell_mint,
534                    buy,
535                    sell_amount,
536                    buy_amount,
537                    valid_to,
538                    flags,
539                    app,
540                )| {
541                    OrderIntent {
542                        owner: Pubkey::new_from_array(owner),
543                        sell: TokenAsset {
544                            mint: Pubkey::new_from_array(sell_mint),
545                            token_account: Pubkey::new_from_array(sell_tok),
546                        },
547                        buy,
548                        sell_amount,
549                        buy_amount,
550                        valid_to,
551                        flags,
552                        app_data: app,
553                    }
554                },
555            )
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use crate::data::intent::fixtures::FLAGS_OFFSET;
562
563    use super::fixtures::sample_intent;
564    use super::*;
565    use crate::fixtures::pubkey_from_seed;
566    use crate::token_program::TokenProgram;
567
568    // Every shape an `OrderIntent` can take on its validated axes: the
569    // `created_on_chain` flag bit, the `kind` enum, and the
570    // `partially_fillable` flag bit.
571    fn all_flag_shapes() -> impl Iterator<Item = OrderIntent> {
572        [false, true].into_iter().flat_map(|created_on_chain| {
573            fixtures::ALL_ORDER_KINDS.into_iter().flat_map(move |kind| {
574                [false, true].into_iter().map(move |partially_fillable| {
575                    sample_intent(Flags {
576                        created_on_chain,
577                        kind,
578                        partially_fillable,
579                    })
580                })
581            })
582        })
583    }
584
585    // Pin each width to the size of the `OrderIntent` field it encodes. The
586    // widths summing to `SIZE` is enforced separately, at compile time, by the
587    // `array_refs!` / `mut_array_refs!` invocations in the codec.
588    #[test]
589    fn widths_match_field_sizes() {
590        use core::mem::{size_of, size_of_val};
591
592        // Any `OrderIntent` works: `size_of_val` only consults the field
593        // type, never the data.
594        let intent = sample_intent(Default::default());
595        let (token_account, mint) = &intent.buy.encode();
596
597        assert_eq!(EncodedOrderIntent::WIDTH_OWNER, size_of_val(&intent.owner));
598        assert_eq!(
599            EncodedOrderIntent::WIDTH_SELL_TOKEN,
600            size_of_val(&intent.sell.token_account)
601        );
602        assert_eq!(
603            EncodedOrderIntent::WIDTH_SELL_MINT,
604            size_of_val(&intent.sell.mint)
605        );
606        assert_eq!(
607            EncodedOrderIntent::WIDTH_BUY_TOKEN,
608            size_of_val(token_account)
609        );
610        assert_eq!(EncodedOrderIntent::WIDTH_BUY_MINT, size_of_val(mint));
611        assert_eq!(
612            EncodedOrderIntent::WIDTH_SELL_AMOUNT,
613            size_of_val(&intent.sell_amount)
614        );
615        assert_eq!(
616            EncodedOrderIntent::WIDTH_BUY_AMOUNT,
617            size_of_val(&intent.buy_amount)
618        );
619        assert_eq!(
620            EncodedOrderIntent::WIDTH_VALID_TO,
621            size_of_val(&intent.valid_to)
622        );
623        assert_eq!(
624            EncodedOrderIntent::WIDTH_FLAGS,
625            // in truth if there was a problem here it would actually cause a compilation error
626            size_of_val::<[u8; 1]>(&Flags::default().into())
627        );
628        assert_eq!(
629            EncodedOrderIntent::WIDTH_APP_DATA,
630            size_of_val(&intent.app_data)
631        );
632
633        assert_eq!(EncodedOrderIntent::SIZE, size_of::<EncodedOrderIntent>());
634    }
635
636    #[test]
637    fn every_flag_owns_a_distinct_bit() {
638        let byte = |flags: Flags| <[u8; 1]>::from(flags)[0];
639        let cleared = Flags {
640            created_on_chain: false,
641            kind: OrderKind::Sell,
642            partially_fillable: false,
643        };
644        assert_eq!(byte(cleared), 0);
645
646        let set_one_by_one = [
647            (
648                Flags::CREATED_ON_CHAIN,
649                Flags {
650                    created_on_chain: true,
651                    ..cleared
652                },
653            ),
654            (
655                Flags::KIND,
656                Flags {
657                    kind: OrderKind::Buy,
658                    ..cleared
659                },
660            ),
661            (
662                Flags::PARTIALLY_FILLABLE,
663                Flags {
664                    partially_fillable: true,
665                    ..cleared
666                },
667            ),
668        ];
669        let mut seen = 0u8;
670        for (bit, flags) in set_one_by_one {
671            assert_eq!(bit.count_ones(), 1, "a flag must occupy a single bit");
672            assert_eq!(seen & bit, 0, "two flags must not share a bit");
673            assert!(
674                bit > seen,
675                "each flag must be more significant than the ones before it"
676            );
677            seen |= bit;
678            assert_eq!(byte(flags), bit);
679        }
680        assert_eq!(seen, Flags::DEFINED);
681    }
682
683    #[test]
684    fn roundtrip_all_kind_and_flag_combinations() {
685        for intent in all_flag_shapes() {
686            let encoded = EncodedOrderIntent::from(&intent);
687            let decoded = OrderIntent::try_from(&*encoded).expect("example must decode");
688            assert_eq!(decoded, intent);
689        }
690    }
691
692    #[test]
693    fn decode_accepts_defined_flag_bits_only() {
694        let encoded = EncodedOrderIntent::from(&sample_intent(Default::default()));
695        let mut bytes: [u8; EncodedOrderIntent::SIZE] = *encoded;
696        for flags in u8::MIN..=u8::MAX {
697            bytes[FLAGS_OFFSET] = flags;
698            let decoded = OrderIntent::try_from(&bytes);
699            if flags & !Flags::DEFINED != 0 {
700                assert_eq!(
701                    decoded.err(),
702                    Some(ProgramError::InvalidInstructionData),
703                    "flags {flags:#04x} sets a reserved bit and must be rejected",
704                );
705            }
706        }
707    }
708
709    #[test]
710    fn uid_digest_regression() {
711        // Compared as hex: `Hash`'s `Display` and `Debug` are both base58,
712        // which is not how we represent order UIDs elsewhere.
713        fn hex(bytes: &[u8]) -> String {
714            bytes.iter().map(|b| format!("{b:02x}")).collect()
715        }
716        let intent = sample_intent(Flags {
717            created_on_chain: true,
718            kind: OrderKind::Buy,
719            partially_fillable: true,
720        });
721        assert_eq!(
722            hex(intent.uid().as_ref()),
723            "de4096c6c100056f1e4636ea4fafefad40fc1d0b37692fe3ca1e0db3644b86bd",
724        );
725    }
726
727    #[test]
728    fn encoding_regression() {
729        let encoded = EncodedOrderIntent::from(&sample_intent(Flags {
730            created_on_chain: true,
731            kind: OrderKind::Buy,
732            partially_fillable: true,
733        }));
734        let encoding: [u8; EncodedOrderIntent::SIZE] = *encoded;
735        #[rustfmt::skip]
736        let expected: [u8; EncodedOrderIntent::SIZE] = [
737            // owner ([0x11; 32])
738            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
739            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
740            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
741            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
742            // sell_token_account ([0x22; 32])
743            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
744            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
745            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
746            0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
747            // sell_mint ([0x33; 32])
748            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
749            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
750            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
751            0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
752            // buy_token_account ([0x44; 32])
753            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
754            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
755            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
756            0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
757            // buy_mint ([0x55; 32])
758            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
759            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
760            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
761            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
762            // sell_amount (0x0123_4567_89ab_cdef, LE u64)
763            0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
764            // buy_amount (0xfedc_ba98_7654_3210, LE u64)
765            0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
766            // valid_to (0xdead_beef, LE u32)
767            0xef, 0xbe, 0xad, 0xde,
768            // flags (created_on_chain | kind (Buy = 1) | partially_fillable)
769            0b00000111,
770            // app_data ([0x66; 32])
771            0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
772            0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
773            0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
774            0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
775        ];
776        assert_eq!(encoding, expected);
777    }
778
779    #[test]
780    fn an_asset_lowers_to_the_pair_the_wire_carries() {
781        let mint = pubkey_from_seed("mint");
782        let account = pubkey_from_seed("account");
783
784        for (buy, expected_mint) in [
785            (
786                Asset::try_from(TokenAsset {
787                    mint,
788                    token_account: account,
789                })
790                .expect("not native SOL"),
791                mint,
792            ),
793            (Asset::Native(account), ENCODED_NATIVE_SOL_TRANSFER),
794        ] {
795            let encoded = EncodedOrderIntent::from(&OrderIntent {
796                buy,
797                ..sample_intent(Default::default())
798            });
799            assert_eq!(intent_slots(&encoded).buy_mint, expected_mint.as_array());
800            assert_eq!(intent_slots(&encoded).buy_token, account.as_array());
801        }
802    }
803
804    #[test]
805    fn native_sol_mint_is_the_system_program() {
806        assert!(Asset::is_native_sol(ENCODED_NATIVE_SOL_TRANSFER.as_array()));
807        for program in TokenProgram::ALL {
808            assert!(!Asset::is_native_sol(program.address().as_array()));
809        }
810    }
811
812    #[test]
813    fn an_spl_mint_is_not_native_sol() {
814        assert!(!Asset::is_native_sol(
815            pubkey_from_seed("some mint").as_array()
816        ));
817    }
818
819    #[test]
820    fn a_token_side_naming_the_native_marker_is_native_sol() {
821        let account = pubkey_from_seed("some account");
822        assert_eq!(
823            Asset::decode(ENCODED_NATIVE_SOL_TRANSFER, account),
824            Asset::Native(account),
825        );
826    }
827
828    #[test]
829    fn a_token_side_naming_the_native_marker_is_not_a_token_asset() {
830        let token = TokenAsset {
831            mint: ENCODED_NATIVE_SOL_TRANSFER,
832            token_account: Pubkey::new_from_array([0x44; 32]),
833        };
834        assert_eq!(Asset::try_from(token), Err(NativeSolHasNoMint));
835    }
836
837    #[test]
838    fn default_intent_is_the_all_zero_encoding() {
839        let intent = OrderIntent::default();
840        let encoded = EncodedOrderIntent::from(&intent);
841        assert_eq!(*encoded, [0u8; EncodedOrderIntent::SIZE]);
842    }
843
844    // Property-based tests, non-deterministic.
845    mod proptest {
846        use ::proptest::{prelude::*, test_runner::TestCaseError};
847
848        use super::*;
849        use crate::data::intent::fixtures::{
850            arb_asset, arb_flags_byte, arb_invalid_flags_byte, arb_order_intent, FLAGS_OFFSET,
851        };
852
853        proptest! {
854
855            #[test]
856            fn unique_intents_do_not_share_uid(intent_a in arb_order_intent(), intent_b in arb_order_intent()) {
857                prop_assume!(intent_a != intent_b);
858                prop_assert_ne!(intent_a.uid(), intent_b.uid());
859            }
860
861            // For any `OrderIntent`, encoding an intent into an encoded
862            // intent and then decoding it returns the same intent.
863            #[test]
864            fn intent_roundtrip(intent in arb_order_intent()) {
865                let encoded = EncodedOrderIntent::from(&intent);
866                let decoded = OrderIntent::try_from(&*encoded)
867                    .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
868                prop_assert_eq!(decoded, intent);
869            }
870
871            // For any bytes whose flags slot is valid, decoding and then
872            // re-encoding produces back the original bytes.
873            #[test]
874            fn bytes_roundtrip(
875                mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
876                flags in arb_flags_byte(),
877            ) {
878                bytes[FLAGS_OFFSET] = flags;
879                let intent = OrderIntent::try_from(&bytes)
880                    .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
881                prop_assert_eq!(*EncodedOrderIntent::from(&intent), bytes);
882            }
883
884            // Symmetric: any bytes whose flags byte carries a reserved bit
885            // return `InvalidInstructionData`.
886            #[test]
887            fn rejects_reserved_flag_bits(
888                mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
889                bad_flags in arb_invalid_flags_byte(),
890            ) {
891                bytes[FLAGS_OFFSET] = bad_flags;
892                prop_assert_eq!(
893                    OrderIntent::try_from(&bytes),
894                    Err(ProgramError::InvalidInstructionData),
895                );
896            }
897
898            #[test]
899            fn round_trip_encode(asset in arb_asset()) {
900                let (mint, account) = asset.encode();
901                let decoded_asset = Asset::decode(mint, account);
902                prop_assert_eq!(decoded_asset, asset);
903            }
904
905            #[test]
906            fn round_trip_decode(
907                mint in prop_oneof![
908                    any::<[u8; 32]>(),
909                    Just(ENCODED_NATIVE_SOL_TRANSFER.to_bytes()),
910                ],
911                account in any::<[u8; 32]>(),
912            ) {
913                let mint = Pubkey::new_from_array(mint);
914                let account = Pubkey::new_from_array(account);
915                let asset = Asset::decode(mint, account);
916                let (encoded_mint, encoded_account) = asset.encode();
917                prop_assert_eq!(encoded_mint, mint);
918                prop_assert_eq!(encoded_account, account);
919            }
920        }
921    }
922}