1use 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
27pub const ENCODED_NATIVE_SOL_TRANSFER: Pubkey = solana_system_interface::program::ID;
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
34#[repr(u8)]
35pub enum OrderKind {
36 #[default]
37 Sell = 0,
38 Buy = 1,
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
43pub struct Flags {
44 pub kind: OrderKind,
48
49 pub partially_fillable: bool,
54}
55
56impl Flags {
57 const KIND: u8 = 1 << 1;
59 const PARTIALLY_FILLABLE: u8 = 1 << 2;
60
61 const DEFINED: u8 = Self::KIND | Self::PARTIALLY_FILLABLE;
63}
64
65impl From<Flags> for [u8; 1] {
66 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 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#[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#[derive(Clone, Debug, Eq, PartialEq)]
116pub enum Asset {
117 Native(Pubkey),
120
121 TokenProgram(TokenAsset),
123}
124
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub struct NativeSolHasNoMint;
130
131impl TryFrom<TokenAsset> for Asset {
132 type Error = NativeSolHasNoMint;
133
134 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 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 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#[derive(Clone, Debug, Eq, PartialEq)]
174pub struct OrderIntent {
175 pub owner: Pubkey,
178
179 pub sell: TokenAsset,
185
186 pub buy: Asset,
189
190 pub sell_amount: NonZeroU64,
194
195 pub buy_amount: NonZeroU64,
199
200 pub valid_to: u32,
203
204 pub flags: Flags,
207
208 pub app_data: [u8; 32],
212}
213
214#[derive(Clone, Debug, Deref, Eq, PartialEq)]
233pub struct EncodedOrderIntent([u8; Self::SIZE]);
234
235impl EncodedOrderIntent {
236 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 pub fn hash(&self) -> Hash {
252 hash_bytes(&self.0)
253 }
254}
255
256#[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#[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#[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 fn from(intent: &OrderIntent) -> Self {
339 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 fn try_from(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Result<Self, Self::Error> {
388 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 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 pub const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];
435
436 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 fn default() -> Self {
445 Asset::Native(Pubkey::default())
446 }
447 }
448
449 impl Default for OrderIntent {
450 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 pub fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
487 Union::new(ALL_ORDER_KINDS.map(Just))
488 }
489
490 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 pub fn arb_flags_byte() -> impl Strategy<Value = u8> {
500 any::<u8>().prop_map(|byte| byte & Flags::DEFINED)
501 }
502
503 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 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 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 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 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 #[test]
613 fn widths_match_field_sizes() {
614 use core::mem::{size_of, size_of_val};
615
616 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 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 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 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 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 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 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 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 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
778 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
780 0xef, 0xbe, 0xad, 0xde,
782 0b00000110,
784 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 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 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 #[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 #[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 #[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 #[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}