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
24#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
27#[repr(u8)]
28pub enum OrderKind {
29 #[default]
30 Sell = 0,
31 Buy = 1,
32}
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
36pub struct Flags {
37 pub created_on_chain: bool,
42
43 pub kind: OrderKind,
47
48 pub partially_fillable: bool,
53}
54
55impl Flags {
56 const CREATED_ON_CHAIN: u8 = 1 << 0;
58 const KIND: u8 = 1 << 1;
59 const PARTIALLY_FILLABLE: u8 = 1 << 2;
60
61 const DEFINED: u8 = Self::CREATED_ON_CHAIN | 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.created_on_chain {
70 byte |= Flags::CREATED_ON_CHAIN;
71 }
72 if flags.kind == OrderKind::Buy {
73 byte |= Flags::KIND;
74 }
75 if flags.partially_fillable {
76 byte |= Flags::PARTIALLY_FILLABLE;
77 }
78 [byte]
79 }
80}
81
82impl TryFrom<[u8; 1]> for Flags {
83 type Error = ProgramError;
84
85 fn try_from(bytes: [u8; 1]) -> Result<Self, Self::Error> {
90 let [byte] = bytes;
91 if byte & !Self::DEFINED != 0 {
92 return Err(ProgramError::InvalidInstructionData);
93 }
94 Ok(Flags {
95 created_on_chain: byte & Self::CREATED_ON_CHAIN != 0,
96 kind: if byte & Self::KIND == 0 {
97 OrderKind::Sell
98 } else {
99 OrderKind::Buy
100 },
101 partially_fillable: byte & Self::PARTIALLY_FILLABLE != 0,
102 })
103 }
104}
105
106#[derive(Clone, Debug, Eq, PartialEq, Default)]
107pub struct OrderIntent {
108 pub owner: Pubkey,
112
113 pub buy_token_account: Pubkey,
116
117 pub buy_mint: Pubkey,
119
120 pub sell_token_account: Pubkey,
126
127 pub sell_mint: Pubkey,
129
130 pub sell_amount: u64,
134
135 pub buy_amount: u64,
139
140 pub valid_to: u32,
143
144 pub flags: Flags,
147
148 pub app_data: [u8; 32],
152}
153
154#[derive(Clone, Debug, Deref, Eq, PartialEq)]
173pub struct EncodedOrderIntent([u8; Self::SIZE]);
174
175impl EncodedOrderIntent {
176 const WIDTH_OWNER: usize = size_of::<Pubkey>();
178 const WIDTH_BUY_TOKEN: usize = size_of::<Pubkey>();
179 const WIDTH_BUY_MINT: usize = size_of::<Pubkey>();
180 const WIDTH_SELL_TOKEN: usize = size_of::<Pubkey>();
181 const WIDTH_SELL_MINT: usize = size_of::<Pubkey>();
182 const WIDTH_SELL_AMOUNT: usize = size_of::<u64>();
183 const WIDTH_BUY_AMOUNT: usize = size_of::<u64>();
184 const WIDTH_VALID_TO: usize = size_of::<u32>();
185 const WIDTH_FLAGS: usize = size_of::<u8>();
186 const WIDTH_APP_DATA: usize = size_of::<[u8; 32]>();
187
188 pub const SIZE: usize = 213;
189
190 pub fn hash(&self) -> Hash {
192 hash_bytes(&self.0)
193 }
194
195 pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderIntent, Hash), ProgramError> {
199 let intent = OrderIntent::try_from(bytes)?;
200 let uid = hash_bytes(bytes);
206 Ok((intent, uid))
207 }
208}
209
210pub fn hash_bytes(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Hash {
211 solana_sha256_hasher::hashv(&[bytes.as_slice()])
212}
213
214impl From<&EncodedOrderIntent> for [u8; EncodedOrderIntent::SIZE] {
215 fn from(encoded: &EncodedOrderIntent) -> Self {
216 encoded.0
217 }
218}
219
220impl From<&OrderIntent> for EncodedOrderIntent {
221 fn from(intent: &OrderIntent) -> Self {
222 let mut out = [0u8; Self::SIZE];
225 let (
226 owner,
227 buy_token,
228 buy_mint,
229 sell_token,
230 sell_mint,
231 sell_amount,
232 buy_amount,
233 valid_to,
234 flags,
235 app_data,
236 ) = mut_array_refs![
237 &mut out,
238 EncodedOrderIntent::WIDTH_OWNER,
239 EncodedOrderIntent::WIDTH_BUY_TOKEN,
240 EncodedOrderIntent::WIDTH_BUY_MINT,
241 EncodedOrderIntent::WIDTH_SELL_TOKEN,
242 EncodedOrderIntent::WIDTH_SELL_MINT,
243 EncodedOrderIntent::WIDTH_SELL_AMOUNT,
244 EncodedOrderIntent::WIDTH_BUY_AMOUNT,
245 EncodedOrderIntent::WIDTH_VALID_TO,
246 EncodedOrderIntent::WIDTH_FLAGS,
247 EncodedOrderIntent::WIDTH_APP_DATA
248 ];
249 *owner = intent.owner.to_bytes();
250 *buy_token = intent.buy_token_account.to_bytes();
251 *buy_mint = intent.buy_mint.to_bytes();
252 *sell_token = intent.sell_token_account.to_bytes();
253 *sell_mint = intent.sell_mint.to_bytes();
254 *sell_amount = intent.sell_amount.to_le_bytes();
255 *buy_amount = intent.buy_amount.to_le_bytes();
256 *valid_to = intent.valid_to.to_le_bytes();
257 *flags = intent.flags.into();
258 *app_data = intent.app_data;
259 Self(out)
260 }
261}
262
263impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
264 type Error = ProgramError;
265
266 fn try_from(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Result<Self, Self::Error> {
267 let (
275 owner,
276 buy_token,
277 buy_mint,
278 sell_token,
279 sell_mint,
280 sell_amount,
281 buy_amount,
282 valid_to,
283 flags,
284 app_data,
285 ) = array_refs![
286 bytes,
287 EncodedOrderIntent::WIDTH_OWNER,
288 EncodedOrderIntent::WIDTH_BUY_TOKEN,
289 EncodedOrderIntent::WIDTH_BUY_MINT,
290 EncodedOrderIntent::WIDTH_SELL_TOKEN,
291 EncodedOrderIntent::WIDTH_SELL_MINT,
292 EncodedOrderIntent::WIDTH_SELL_AMOUNT,
293 EncodedOrderIntent::WIDTH_BUY_AMOUNT,
294 EncodedOrderIntent::WIDTH_VALID_TO,
295 EncodedOrderIntent::WIDTH_FLAGS,
296 EncodedOrderIntent::WIDTH_APP_DATA
297 ];
298
299 Ok(OrderIntent {
300 owner: Pubkey::new_from_array(*owner),
301 buy_token_account: Pubkey::new_from_array(*buy_token),
302 buy_mint: Pubkey::new_from_array(*buy_mint),
303 sell_token_account: Pubkey::new_from_array(*sell_token),
304 sell_mint: Pubkey::new_from_array(*sell_mint),
305 sell_amount: u64::from_le_bytes(*sell_amount),
306 buy_amount: u64::from_le_bytes(*buy_amount),
307 valid_to: u32::from_le_bytes(*valid_to),
308 flags: Flags::try_from(*flags)?,
309 app_data: *app_data,
310 })
311 }
312}
313
314impl TryFrom<&EncodedOrderIntent> for OrderIntent {
315 type Error = ProgramError;
316
317 fn try_from(encoded: &EncodedOrderIntent) -> Result<Self, Self::Error> {
318 OrderIntent::try_from(&encoded.0)
319 }
320}
321
322impl OrderIntent {
323 pub fn uid(&self) -> Hash {
327 EncodedOrderIntent::from(self).hash()
328 }
329}
330
331#[cfg(any(test, feature = "test-fixtures"))]
332pub mod fixtures {
333 use proptest::{prelude::*, strategy::Union};
334
335 use super::{Flags, OrderIntent, OrderKind, Pubkey};
336
337 pub const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];
339
340 pub const FLAGS_OFFSET: usize = 180;
342
343 pub fn sample_intent(flags: Flags) -> OrderIntent {
344 OrderIntent {
345 owner: Pubkey::new_from_array([0x11; 32]),
346 buy_token_account: Pubkey::new_from_array([0x22; 32]),
347 buy_mint: Pubkey::new_from_array([0x33; 32]),
348 sell_token_account: Pubkey::new_from_array([0x44; 32]),
349 sell_mint: Pubkey::new_from_array([0x55; 32]),
350 sell_amount: 0x0123_4567_89ab_cdef,
351 buy_amount: 0xfedc_ba98_7654_3210,
352 valid_to: 0xdead_beef,
353 flags,
354 app_data: [0x66; 32],
355 }
356 }
357
358 pub fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
360 Union::new(ALL_ORDER_KINDS.map(Just))
361 }
362
363 pub fn arb_flags() -> impl Strategy<Value = Flags> {
365 (any::<bool>(), arb_order_kind(), any::<bool>()).prop_map(
366 |(created_on_chain, kind, partially_fillable)| Flags {
367 created_on_chain,
368 kind,
369 partially_fillable,
370 },
371 )
372 }
373
374 pub fn arb_flags_byte() -> impl Strategy<Value = u8> {
376 any::<u8>().prop_map(|byte| byte & Flags::DEFINED)
377 }
378
379 pub fn arb_invalid_flags_byte() -> impl Strategy<Value = u8> {
381 any::<u8>().prop_filter("must have at least one bit that is undefined", |byte| {
382 byte & !Flags::DEFINED > 0
383 })
384 }
385
386 pub fn arb_order_intent() -> impl Strategy<Value = OrderIntent> {
388 (
389 any::<[u8; 32]>(),
390 any::<[u8; 32]>(),
391 any::<[u8; 32]>(),
392 any::<[u8; 32]>(),
393 any::<[u8; 32]>(),
394 any::<u64>(),
395 any::<u64>(),
396 any::<u32>(),
397 arb_flags(),
398 any::<[u8; 32]>(),
399 )
400 .prop_map(
401 |(
402 owner,
403 buy_tok,
404 buy_mint,
405 sell_tok,
406 sell_mint,
407 sell_amount,
408 buy_amount,
409 valid_to,
410 flags,
411 app,
412 )| {
413 OrderIntent {
414 owner: Pubkey::new_from_array(owner),
415 buy_token_account: Pubkey::new_from_array(buy_tok),
416 buy_mint: Pubkey::new_from_array(buy_mint),
417 sell_token_account: Pubkey::new_from_array(sell_tok),
418 sell_mint: Pubkey::new_from_array(sell_mint),
419 sell_amount,
420 buy_amount,
421 valid_to,
422 flags,
423 app_data: app,
424 }
425 },
426 )
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use crate::data::intent::fixtures::FLAGS_OFFSET;
433
434 use super::fixtures::sample_intent;
435 use super::*;
436
437 fn all_flag_shapes() -> impl Iterator<Item = OrderIntent> {
441 [false, true].into_iter().flat_map(|created_on_chain| {
442 fixtures::ALL_ORDER_KINDS.into_iter().flat_map(move |kind| {
443 [false, true].into_iter().map(move |partially_fillable| {
444 sample_intent(Flags {
445 created_on_chain,
446 kind,
447 partially_fillable,
448 })
449 })
450 })
451 })
452 }
453
454 #[test]
458 fn widths_match_field_sizes() {
459 use core::mem::{size_of, size_of_val};
460
461 let intent = sample_intent(Default::default());
464
465 assert_eq!(EncodedOrderIntent::WIDTH_OWNER, size_of_val(&intent.owner));
466 assert_eq!(
467 EncodedOrderIntent::WIDTH_BUY_TOKEN,
468 size_of_val(&intent.buy_token_account)
469 );
470 assert_eq!(
471 EncodedOrderIntent::WIDTH_BUY_MINT,
472 size_of_val(&intent.buy_mint)
473 );
474 assert_eq!(
475 EncodedOrderIntent::WIDTH_SELL_TOKEN,
476 size_of_val(&intent.sell_token_account)
477 );
478 assert_eq!(
479 EncodedOrderIntent::WIDTH_SELL_MINT,
480 size_of_val(&intent.sell_mint)
481 );
482 assert_eq!(
483 EncodedOrderIntent::WIDTH_SELL_AMOUNT,
484 size_of_val(&intent.sell_amount)
485 );
486 assert_eq!(
487 EncodedOrderIntent::WIDTH_BUY_AMOUNT,
488 size_of_val(&intent.buy_amount)
489 );
490 assert_eq!(
491 EncodedOrderIntent::WIDTH_VALID_TO,
492 size_of_val(&intent.valid_to)
493 );
494 assert_eq!(
495 EncodedOrderIntent::WIDTH_FLAGS,
496 size_of_val::<[u8; 1]>(&Flags::default().into())
498 );
499 assert_eq!(
500 EncodedOrderIntent::WIDTH_APP_DATA,
501 size_of_val(&intent.app_data)
502 );
503
504 assert_eq!(EncodedOrderIntent::SIZE, size_of::<EncodedOrderIntent>());
505 }
506
507 #[test]
508 fn every_flag_owns_a_distinct_bit() {
509 let byte = |flags: Flags| <[u8; 1]>::from(flags)[0];
510 let cleared = Flags {
511 created_on_chain: false,
512 kind: OrderKind::Sell,
513 partially_fillable: false,
514 };
515 assert_eq!(byte(cleared), 0);
516
517 let set_one_by_one = [
518 (
519 Flags::CREATED_ON_CHAIN,
520 Flags {
521 created_on_chain: true,
522 ..cleared
523 },
524 ),
525 (
526 Flags::KIND,
527 Flags {
528 kind: OrderKind::Buy,
529 ..cleared
530 },
531 ),
532 (
533 Flags::PARTIALLY_FILLABLE,
534 Flags {
535 partially_fillable: true,
536 ..cleared
537 },
538 ),
539 ];
540 let mut seen = 0u8;
541 for (bit, flags) in set_one_by_one {
542 assert_eq!(bit.count_ones(), 1, "a flag must occupy a single bit");
543 assert_eq!(seen & bit, 0, "two flags must not share a bit");
544 assert!(
545 bit > seen,
546 "each flag must be more significant than the ones before it"
547 );
548 seen |= bit;
549 assert_eq!(byte(flags), bit);
550 }
551 assert_eq!(seen, Flags::DEFINED);
552 }
553
554 #[test]
555 fn roundtrip_all_kind_and_flag_combinations() {
556 for intent in all_flag_shapes() {
557 let encoded = EncodedOrderIntent::from(&intent);
558 let (decoded, _uid) =
559 EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
560 assert_eq!(decoded, intent);
561 }
562 }
563
564 #[test]
569 fn decode_and_hash_uid_matches_encoded_hash() {
570 for intent in all_flag_shapes() {
571 let encoded = EncodedOrderIntent::from(&intent);
572 let (_intent, uid) =
573 EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
574 assert_eq!(uid, encoded.hash());
575 }
576 }
577
578 #[test]
579 fn decode_accepts_defined_flag_bits_only() {
580 let encoded = EncodedOrderIntent::from(&sample_intent(Default::default()));
581 let mut bytes: [u8; EncodedOrderIntent::SIZE] = *encoded;
582 for flags in u8::MIN..=u8::MAX {
583 bytes[FLAGS_OFFSET] = flags;
584 let decoded = EncodedOrderIntent::decode_and_hash(&bytes);
585 if flags & !Flags::DEFINED != 0 {
586 assert_eq!(
587 decoded.err(),
588 Some(ProgramError::InvalidInstructionData),
589 "flags {flags:#04x} sets a reserved bit and must be rejected",
590 );
591 }
592 }
593 }
594
595 #[test]
596 fn uid_digest_regression() {
597 fn hex(bytes: &[u8]) -> String {
600 bytes.iter().map(|b| format!("{b:02x}")).collect()
601 }
602 let intent = sample_intent(Flags {
603 created_on_chain: true,
604 kind: OrderKind::Buy,
605 partially_fillable: true,
606 });
607 assert_eq!(
608 hex(intent.uid().as_ref()),
609 "de4096c6c100056f1e4636ea4fafefad40fc1d0b37692fe3ca1e0db3644b86bd",
610 );
611 }
612
613 #[test]
614 fn encoding_regression() {
615 let encoded = EncodedOrderIntent::from(&sample_intent(Flags {
616 created_on_chain: true,
617 kind: OrderKind::Buy,
618 partially_fillable: true,
619 }));
620 let encoding: [u8; EncodedOrderIntent::SIZE] = *encoded;
621 #[rustfmt::skip]
622 let expected: [u8; EncodedOrderIntent::SIZE] = [
623 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
625 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
626 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
627 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
628 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
630 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
631 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
632 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
633 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
635 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
636 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
637 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
638 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
640 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
641 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
642 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
643 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
645 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
646 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
647 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
648 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
650 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
652 0xef, 0xbe, 0xad, 0xde,
654 0b00000111,
656 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
658 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
659 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
660 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
661 ];
662 assert_eq!(encoding, expected);
663 }
664
665 mod proptest {
667 use ::proptest::{prelude::*, test_runner::TestCaseError};
668
669 use super::*;
670 use crate::data::intent::fixtures::{
671 arb_flags_byte, arb_invalid_flags_byte, arb_order_intent, FLAGS_OFFSET,
672 };
673
674 proptest! {
675 #[test]
680 fn intent_roundtrip(intent in arb_order_intent()) {
681 let encoded = EncodedOrderIntent::from(&intent);
682 let (decoded, uid) = EncodedOrderIntent::decode_and_hash(&encoded)
683 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
684 prop_assert_eq!(decoded, intent);
685 prop_assert_eq!(uid, encoded.hash());
686 }
687
688 #[test]
691 fn bytes_roundtrip(
692 mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
693 flags in arb_flags_byte(),
694 ) {
695 bytes[FLAGS_OFFSET] = flags;
696 let (intent, _uid) = EncodedOrderIntent::decode_and_hash(&bytes)
697 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
698 prop_assert_eq!(*EncodedOrderIntent::from(&intent), bytes);
699 }
700
701 #[test]
704 fn rejects_reserved_flag_bits(
705 mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
706 bad_flags in arb_invalid_flags_byte(),
707 ) {
708 bytes[FLAGS_OFFSET] = bad_flags;
709 prop_assert_eq!(
710 EncodedOrderIntent::decode_and_hash(&bytes),
711 Err(ProgramError::InvalidInstructionData),
712 );
713 }
714 }
715 }
716}