1use 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
24pub const ENCODED_NATIVE_SOL_TRANSFER: Pubkey = solana_system_interface::program::ID;
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
31#[repr(u8)]
32pub enum OrderKind {
33 #[default]
34 Sell = 0,
35 Buy = 1,
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
40pub struct Flags {
41 pub created_on_chain: bool,
46
47 pub kind: OrderKind,
51
52 pub partially_fillable: bool,
57}
58
59impl Flags {
60 const CREATED_ON_CHAIN: u8 = 1 << 0;
62 const KIND: u8 = 1 << 1;
63 const PARTIALLY_FILLABLE: u8 = 1 << 2;
64
65 const DEFINED: u8 = Self::CREATED_ON_CHAIN | Self::KIND | Self::PARTIALLY_FILLABLE;
67}
68
69impl From<Flags> for [u8; 1] {
70 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 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#[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#[derive(Clone, Debug, Eq, PartialEq)]
124pub enum Asset {
125 Native(Pubkey),
128
129 TokenProgram(TokenAsset),
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub struct NativeSolHasNoMint;
138
139impl TryFrom<TokenAsset> for Asset {
140 type Error = NativeSolHasNoMint;
141
142 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 #[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 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 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#[cfg_attr(any(test, feature = "test-fixtures"), derive(Default))]
190#[derive(Clone, Debug, Eq, PartialEq)]
191pub struct OrderIntent {
192 pub owner: Pubkey,
196
197 pub sell: TokenAsset,
203
204 pub buy: Asset,
207
208 pub sell_amount: u64,
212
213 pub buy_amount: u64,
217
218 pub valid_to: u32,
221
222 pub flags: Flags,
225
226 pub app_data: [u8; 32],
230}
231
232#[derive(Clone, Debug, Deref, Eq, PartialEq)]
251pub struct EncodedOrderIntent([u8; Self::SIZE]);
252
253impl EncodedOrderIntent {
254 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 pub fn hash(&self) -> Hash {
270 hash_bytes(&self.0)
271 }
272}
273
274#[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#[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#[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 fn from(intent: &OrderIntent) -> Self {
352 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 fn try_from(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Result<Self, Self::Error> {
401 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 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 pub const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];
443
444 pub const FLAGS_OFFSET: usize = 180;
446
447 impl Default for Asset {
448 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 pub fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
475 Union::new(ALL_ORDER_KINDS.map(Just))
476 }
477
478 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 pub fn arb_flags_byte() -> impl Strategy<Value = u8> {
491 any::<u8>().prop_map(|byte| byte & Flags::DEFINED)
492 }
493
494 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 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 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 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 #[test]
589 fn widths_match_field_sizes() {
590 use core::mem::{size_of, size_of_val};
591
592 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 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 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 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 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 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 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 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 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
764 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
766 0xef, 0xbe, 0xad, 0xde,
768 0b00000111,
770 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 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 #[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 #[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 #[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}