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