use core::mem::size_of;
use arrayref::{array_refs, mut_array_refs};
use derive_more::Deref;
use solana_hash::Hash;
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
#[repr(u8)]
pub enum OrderKind {
#[default]
Sell = 0,
Buy = 1,
}
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct OrderIntent {
pub owner: Pubkey,
pub buy_token_account: Pubkey,
pub sell_token_account: Pubkey,
pub sell_amount: u64,
pub buy_amount: u64,
pub valid_to: u32,
pub kind: OrderKind,
pub partially_fillable: bool,
pub app_data: [u8; 32],
}
#[derive(Clone, Debug, Deref, Eq, PartialEq)]
pub struct EncodedOrderIntent([u8; Self::SIZE]);
impl EncodedOrderIntent {
const WIDTH_OWNER: usize = size_of::<Pubkey>();
const WIDTH_BUY_TOKEN: usize = size_of::<Pubkey>();
const WIDTH_SELL_TOKEN: usize = size_of::<Pubkey>();
const WIDTH_SELL_AMOUNT: usize = size_of::<u64>();
const WIDTH_BUY_AMOUNT: usize = size_of::<u64>();
const WIDTH_VALID_TO: usize = size_of::<u32>();
const WIDTH_KIND: usize = size_of::<OrderKind>();
const WIDTH_PARTIALLY_FILLABLE: usize = size_of::<bool>();
const WIDTH_APP_DATA: usize = size_of::<[u8; 32]>();
pub const SIZE: usize = 150;
pub fn hash(&self) -> Hash {
hash_bytes(&self.0)
}
pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderIntent, Hash), ProgramError> {
let intent = OrderIntent::try_from(bytes)?;
let uid = hash_bytes(bytes);
Ok((intent, uid))
}
}
pub fn hash_bytes(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Hash {
solana_sha256_hasher::hashv(&[bytes.as_slice()])
}
impl From<&EncodedOrderIntent> for [u8; EncodedOrderIntent::SIZE] {
fn from(encoded: &EncodedOrderIntent) -> Self {
encoded.0
}
}
impl From<&OrderIntent> for EncodedOrderIntent {
fn from(intent: &OrderIntent) -> Self {
let mut out = [0u8; Self::SIZE];
let (
owner,
buy_token,
sell_token,
sell_amount,
buy_amount,
valid_to,
kind,
partially_fillable,
app_data,
) = mut_array_refs![
&mut out,
EncodedOrderIntent::WIDTH_OWNER,
EncodedOrderIntent::WIDTH_BUY_TOKEN,
EncodedOrderIntent::WIDTH_SELL_TOKEN,
EncodedOrderIntent::WIDTH_SELL_AMOUNT,
EncodedOrderIntent::WIDTH_BUY_AMOUNT,
EncodedOrderIntent::WIDTH_VALID_TO,
EncodedOrderIntent::WIDTH_KIND,
EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
EncodedOrderIntent::WIDTH_APP_DATA
];
*owner = intent.owner.to_bytes();
*buy_token = intent.buy_token_account.to_bytes();
*sell_token = intent.sell_token_account.to_bytes();
*sell_amount = intent.sell_amount.to_le_bytes();
*buy_amount = intent.buy_amount.to_le_bytes();
*valid_to = intent.valid_to.to_le_bytes();
*kind = [intent.kind as u8];
*partially_fillable = [intent.partially_fillable as u8];
*app_data = intent.app_data;
Self(out)
}
}
impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
type Error = ProgramError;
fn try_from(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Result<Self, Self::Error> {
let (
owner,
buy_token,
sell_token,
sell_amount,
buy_amount,
valid_to,
kind,
partially_fillable,
app_data,
) = array_refs![
bytes,
EncodedOrderIntent::WIDTH_OWNER,
EncodedOrderIntent::WIDTH_BUY_TOKEN,
EncodedOrderIntent::WIDTH_SELL_TOKEN,
EncodedOrderIntent::WIDTH_SELL_AMOUNT,
EncodedOrderIntent::WIDTH_BUY_AMOUNT,
EncodedOrderIntent::WIDTH_VALID_TO,
EncodedOrderIntent::WIDTH_KIND,
EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
EncodedOrderIntent::WIDTH_APP_DATA
];
Ok(OrderIntent {
owner: Pubkey::new_from_array(*owner),
buy_token_account: Pubkey::new_from_array(*buy_token),
sell_token_account: Pubkey::new_from_array(*sell_token),
sell_amount: u64::from_le_bytes(*sell_amount),
buy_amount: u64::from_le_bytes(*buy_amount),
valid_to: u32::from_le_bytes(*valid_to),
kind: match kind {
[0] => OrderKind::Sell,
[1] => OrderKind::Buy,
_ => return Err(ProgramError::InvalidInstructionData),
},
partially_fillable: match partially_fillable {
[0] => false,
[1] => true,
_ => return Err(ProgramError::InvalidInstructionData),
},
app_data: *app_data,
})
}
}
impl TryFrom<&EncodedOrderIntent> for OrderIntent {
type Error = ProgramError;
fn try_from(encoded: &EncodedOrderIntent) -> Result<Self, Self::Error> {
OrderIntent::try_from(&encoded.0)
}
}
impl OrderIntent {
pub fn uid(&self) -> Hash {
EncodedOrderIntent::from(self).hash()
}
}
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
use proptest::{prelude::*, strategy::Union};
use super::{EncodedOrderIntent, OrderIntent, OrderKind, Pubkey};
pub const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];
pub const KIND_OFFSET: usize = 116;
pub const PARTIALLY_FILLABLE_OFFSET: usize = KIND_OFFSET + EncodedOrderIntent::WIDTH_KIND;
pub fn sample_intent(kind: OrderKind, partially_fillable: bool) -> OrderIntent {
OrderIntent {
owner: Pubkey::new_from_array([0x11; 32]),
buy_token_account: Pubkey::new_from_array([0x22; 32]),
sell_token_account: Pubkey::new_from_array([0x33; 32]),
sell_amount: 0x0123_4567_89ab_cdef,
buy_amount: 0xfedc_ba98_7654_3210,
valid_to: 0xdead_beef,
kind,
partially_fillable,
app_data: [0x44; 32],
}
}
pub fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
Union::new(ALL_ORDER_KINDS.map(Just))
}
pub fn arb_order_intent() -> impl Strategy<Value = OrderIntent> {
(
any::<[u8; 32]>(),
any::<[u8; 32]>(),
any::<[u8; 32]>(),
any::<u64>(),
any::<u64>(),
any::<u32>(),
arb_order_kind(),
any::<bool>(),
any::<[u8; 32]>(),
)
.prop_map(
|(owner, buy_tok, sell_tok, sell_amount, buy_amount, valid_to, kind, pf, app)| {
OrderIntent {
owner: Pubkey::new_from_array(owner),
buy_token_account: Pubkey::new_from_array(buy_tok),
sell_token_account: Pubkey::new_from_array(sell_tok),
sell_amount,
buy_amount,
valid_to,
kind,
partially_fillable: pf,
app_data: app,
}
},
)
}
}
#[cfg(test)]
mod tests {
use hex_literal::hex;
use super::fixtures::{sample_intent, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET};
use super::*;
fn all_kind_and_fillable() -> impl Iterator<Item = (OrderKind, bool)> {
fixtures::ALL_ORDER_KINDS
.into_iter()
.flat_map(|kind| core::iter::repeat(kind).zip([false, true]))
}
#[test]
fn widths_match_field_sizes() {
use core::mem::{size_of, size_of_val};
let intent = sample_intent(OrderKind::Sell, false);
assert_eq!(EncodedOrderIntent::WIDTH_OWNER, size_of_val(&intent.owner));
assert_eq!(
EncodedOrderIntent::WIDTH_BUY_TOKEN,
size_of_val(&intent.buy_token_account)
);
assert_eq!(
EncodedOrderIntent::WIDTH_SELL_TOKEN,
size_of_val(&intent.sell_token_account)
);
assert_eq!(
EncodedOrderIntent::WIDTH_SELL_AMOUNT,
size_of_val(&intent.sell_amount)
);
assert_eq!(
EncodedOrderIntent::WIDTH_BUY_AMOUNT,
size_of_val(&intent.buy_amount)
);
assert_eq!(
EncodedOrderIntent::WIDTH_VALID_TO,
size_of_val(&intent.valid_to)
);
assert_eq!(EncodedOrderIntent::WIDTH_KIND, size_of_val(&intent.kind));
assert_eq!(
EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
size_of_val(&intent.partially_fillable)
);
assert_eq!(
EncodedOrderIntent::WIDTH_APP_DATA,
size_of_val(&intent.app_data)
);
assert_eq!(EncodedOrderIntent::SIZE, size_of::<EncodedOrderIntent>());
}
#[test]
fn roundtrip_all_kind_and_bool_combinations() {
for (kind, partially_fillable) in all_kind_and_fillable() {
let intent = sample_intent(kind, partially_fillable);
let encoded = EncodedOrderIntent::from(&intent);
let (decoded, _uid) =
EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
assert_eq!(decoded, intent);
}
}
#[test]
fn decode_and_hash_uid_matches_encoded_hash() {
for (kind, partially_fillable) in all_kind_and_fillable() {
let encoded = EncodedOrderIntent::from(&sample_intent(kind, partially_fillable));
let (_intent, uid) =
EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
assert_eq!(uid, encoded.hash());
}
}
#[test]
fn sanity_check_offsets() {
fn first_differing_byte(lhs: &[u8], rhs: &[u8]) -> Option<usize> {
lhs.iter().zip(rhs).position(|(l, r)| l != r)
}
let sell_false: EncodedOrderIntent = (&sample_intent(OrderKind::Sell, false)).into();
let sell_true: EncodedOrderIntent = (&sample_intent(OrderKind::Sell, true)).into();
let buy_true: EncodedOrderIntent = (&sample_intent(OrderKind::Buy, true)).into();
assert_eq!(
first_differing_byte(sell_false.as_slice(), sell_true.as_slice())
.expect("should have different partially fillable byte"),
PARTIALLY_FILLABLE_OFFSET
);
assert_eq!(
first_differing_byte(buy_true.as_slice(), sell_true.as_slice())
.expect("should have different kind byte"),
KIND_OFFSET
);
}
#[test]
fn decode_rejects_out_of_range_kind() {
let encoded = EncodedOrderIntent::from(&sample_intent(OrderKind::Sell, false));
let mut bytes: [u8; EncodedOrderIntent::SIZE] = *encoded;
for bad in 0x02u8..=0xff {
bytes[KIND_OFFSET] = bad;
let err = EncodedOrderIntent::decode_and_hash(&bytes)
.expect_err("should reject out of range kind");
assert_eq!(err, ProgramError::InvalidInstructionData);
}
}
#[test]
fn decode_rejects_non_boolean_partially_fillable() {
let encoded = EncodedOrderIntent::from(&sample_intent(OrderKind::Sell, false));
let mut bytes: [u8; EncodedOrderIntent::SIZE] = *encoded;
for bad in 0x02u8..=0xff {
bytes[PARTIALLY_FILLABLE_OFFSET] = bad;
let err = EncodedOrderIntent::decode_and_hash(&bytes)
.expect_err("should reject out of range partially fillable");
assert_eq!(err, ProgramError::InvalidInstructionData);
}
}
#[test]
fn uid_digest_regression() {
let intent = sample_intent(OrderKind::Buy, true);
let expected = hex!("7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00");
assert_eq!(intent.uid(), Hash::from(expected));
}
#[test]
#[rustfmt::skip]
fn encoding_regression() {
let encoded = EncodedOrderIntent::from(&sample_intent(OrderKind::Buy, true));
let encoding: [u8; EncodedOrderIntent::SIZE] = *encoded;
let expected: [u8; EncodedOrderIntent::SIZE] = [
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
0xef, 0xbe, 0xad, 0xde,
0x01,
0x01,
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
];
assert_eq!(encoding, expected);
}
mod proptest {
use ::proptest::{prelude::*, test_runner::TestCaseError};
use super::*;
use crate::data::intent::fixtures::{
arb_order_intent, arb_order_kind, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET,
};
fn arb_bad_order_kind_byte() -> impl Strategy<Value = u8> {
2u8..=255
}
fn arb_bad_bool_byte() -> impl Strategy<Value = u8> {
2u8..=255
}
proptest! {
#[test]
fn intent_roundtrip(intent in arb_order_intent()) {
let encoded = EncodedOrderIntent::from(&intent);
let (decoded, uid) = EncodedOrderIntent::decode_and_hash(&encoded)
.map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
prop_assert_eq!(decoded, intent);
prop_assert_eq!(uid, encoded.hash());
}
#[test]
fn bytes_roundtrip(
mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
kind in arb_order_kind(),
partially_fillable in any::<bool>(),
) {
bytes[KIND_OFFSET] = kind as u8;
bytes[PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
let (intent, _uid) = EncodedOrderIntent::decode_and_hash(&bytes)
.map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
prop_assert_eq!(*EncodedOrderIntent::from(&intent), bytes);
}
#[test]
fn rejects_invalid_kind_byte(
mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
bad_kind in arb_bad_order_kind_byte(),
partially_fillable in any::<bool>(),
) {
bytes[KIND_OFFSET] = bad_kind;
bytes[PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
prop_assert_eq!(
EncodedOrderIntent::decode_and_hash(&bytes),
Err(ProgramError::InvalidInstructionData),
);
}
#[test]
fn rejects_invalid_partially_fillable_byte(
mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
kind in arb_order_kind(),
bad_pf in arb_bad_bool_byte(),
) {
bytes[KIND_OFFSET] = kind as u8;
bytes[PARTIALLY_FILLABLE_OFFSET] = bad_pf;
prop_assert_eq!(
EncodedOrderIntent::decode_and_hash(&bytes),
Err(ProgramError::InvalidInstructionData),
);
}
}
}
}