use core::mem::size_of;
use arrayref::{array_refs, mut_array_refs};
use derive_more::Deref;
use solana_account_view::AccountView;
use solana_address::Address;
use solana_hash::Hash;
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;
use crate::data::intent::{self, EncodedOrderIntent, OrderIntent, OrderKind};
use crate::pda::is_pda_with_signer_seeds;
use crate::pda::order::order_pda_signer_seeds;
use crate::{SettlementAccount, SettlementError};
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct OrderAccount {
pub bump: u8,
pub cancelled: bool,
pub amount_withdrawn: u64,
pub amount_received: u64,
pub created_by: Pubkey,
pub intent: OrderIntent,
}
impl OrderAccount {
pub fn load_from_pda(
order_pda: &AccountView,
program_id: &Address,
) -> Result<Self, ProgramError> {
let (account, uid) = {
let data = order_pda.try_borrow()?;
let bytes: &[u8; EncodedOrderAccount::SIZE] = (&*data)
.try_into()
.map_err(|_| ProgramError::InvalidAccountData)?;
EncodedOrderAccount::decode_and_hash(bytes)?
};
if !is_pda_with_signer_seeds(
order_pda.address(),
program_id,
order_pda_signer_seeds(&uid, &[account.bump]),
) {
return Err(SettlementError::AccountNotDerivable.into());
}
Ok(account)
}
pub fn is_fully_filled(&self) -> bool {
let (filled, order_amount) =
fill_progress(&self.intent, self.amount_withdrawn, self.amount_received);
filled >= order_amount
}
}
pub fn fill_progress(
intent: &OrderIntent,
amount_withdrawn: u64,
amount_received: u64,
) -> (u64, u64) {
match intent.flags.kind {
OrderKind::Sell => (amount_withdrawn, intent.sell_amount),
OrderKind::Buy => (amount_received, intent.buy_amount),
}
}
#[derive(Clone, Debug, Deref, Eq, PartialEq)]
pub struct EncodedOrderAccount([u8; Self::SIZE]);
impl EncodedOrderAccount {
const W_DISCRIMINATOR: usize = size_of::<u8>();
const W_BUMP: usize = size_of::<u8>();
const W_CANCELLED: usize = size_of::<bool>();
const W_AMOUNT_WITHDRAWN: usize = size_of::<u64>();
const W_AMOUNT_RECEIVED: usize = size_of::<u64>();
const W_CREATED_BY: usize = size_of::<Pubkey>();
const W_INTENT: usize = EncodedOrderIntent::SIZE;
pub const SIZE: usize = 264;
pub const DISCRIMINATOR: u8 = SettlementAccount::OrderAccount.discriminator();
pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderAccount, Hash), ProgramError> {
let order_account = OrderAccount::try_from(*bytes)?;
let (_, raw_intent) = array_refs![
bytes,
EncodedOrderAccount::SIZE - EncodedOrderAccount::W_INTENT,
EncodedOrderAccount::W_INTENT
];
let intent_uid = intent::hash_bytes(raw_intent);
Ok((order_account, intent_uid))
}
}
pub fn write_account(
buffer: &mut [u8; EncodedOrderAccount::SIZE],
bump: u8,
cancelled: bool,
amount_withdrawn: u64,
amount_received: u64,
created_by: &Pubkey,
encoded_intent: &[u8; EncodedOrderIntent::SIZE],
) {
let (
discriminator_slot,
bump_slot,
cancelled_slot,
amount_withdrawn_slot,
amount_received_slot,
created_by_slot,
intent_slot,
) = mut_array_refs![
buffer,
EncodedOrderAccount::W_DISCRIMINATOR,
EncodedOrderAccount::W_BUMP,
EncodedOrderAccount::W_CANCELLED,
EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
EncodedOrderAccount::W_AMOUNT_RECEIVED,
EncodedOrderAccount::W_CREATED_BY,
EncodedOrderAccount::W_INTENT
];
*discriminator_slot = [EncodedOrderAccount::DISCRIMINATOR];
*bump_slot = [bump];
*cancelled_slot = [cancelled as u8];
*amount_withdrawn_slot = amount_withdrawn.to_le_bytes();
*amount_received_slot = amount_received.to_le_bytes();
*created_by_slot = created_by.to_bytes();
*intent_slot = *encoded_intent;
}
impl From<EncodedOrderAccount> for [u8; EncodedOrderAccount::SIZE] {
fn from(encoded: EncodedOrderAccount) -> Self {
encoded.0
}
}
impl From<OrderAccount> for EncodedOrderAccount {
fn from(account: OrderAccount) -> Self {
let mut out = [0u8; Self::SIZE];
write_account(
&mut out,
account.bump,
account.cancelled,
account.amount_withdrawn,
account.amount_received,
&account.created_by,
&EncodedOrderIntent::from(&account.intent),
);
Self(out)
}
}
impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount {
type Error = ProgramError;
fn try_from(bytes: [u8; EncodedOrderAccount::SIZE]) -> Result<Self, Self::Error> {
let (discriminator, bump, cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![
&bytes,
EncodedOrderAccount::W_DISCRIMINATOR,
EncodedOrderAccount::W_BUMP,
EncodedOrderAccount::W_CANCELLED,
EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
EncodedOrderAccount::W_AMOUNT_RECEIVED,
EncodedOrderAccount::W_CREATED_BY,
EncodedOrderAccount::W_INTENT
];
if *discriminator != [EncodedOrderAccount::DISCRIMINATOR] {
return Err(ProgramError::InvalidAccountData);
}
Ok(OrderAccount {
bump: bump[0],
cancelled: match cancelled {
[0] => false,
[1] => true,
_ => return Err(ProgramError::InvalidAccountData),
},
amount_withdrawn: u64::from_le_bytes(*amount_withdrawn),
amount_received: u64::from_le_bytes(*amount_received),
created_by: Pubkey::new_from_array(*created_by),
intent: OrderIntent::try_from(intent).map_err(|_| ProgramError::InvalidAccountData)?,
})
}
}
impl TryFrom<&[u8]> for OrderAccount {
type Error = ProgramError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let bytes: &[u8; EncodedOrderAccount::SIZE] = bytes
.try_into()
.map_err(|_| ProgramError::InvalidAccountData)?;
OrderAccount::try_from(*bytes)
}
}
impl TryFrom<EncodedOrderAccount> for OrderAccount {
type Error = ProgramError;
fn try_from(encoded: EncodedOrderAccount) -> Result<Self, Self::Error> {
OrderAccount::try_from(encoded.0)
}
}
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
use proptest::prelude::*;
use super::{OrderAccount, Pubkey};
use crate::data::intent::fixtures::{arb_order_intent, sample_intent};
pub const DISCRIMINATOR_OFFSET: usize = 0;
pub const CANCELLED_OFFSET: usize = 2;
pub const INTENT_OFFSET: usize = 51;
pub fn sample_account(cancelled: bool) -> OrderAccount {
OrderAccount {
bump: 0xfd,
cancelled,
amount_withdrawn: 0x0112_2334_4556_6778,
amount_received: 0x899a_abbc_cdde_eff0,
created_by: Pubkey::new_from_array([0x43; 32]),
intent: sample_intent(Default::default()),
}
}
pub fn arb_order_account() -> impl Strategy<Value = OrderAccount> {
(
any::<u8>(),
any::<bool>(),
any::<u64>(),
any::<u64>(),
any::<[u8; 32]>(),
arb_order_intent(),
)
.prop_map(
|(bump, cancelled, amount_withdrawn, amount_received, created_by, intent)| {
OrderAccount {
bump,
cancelled,
amount_withdrawn,
amount_received,
created_by: Pubkey::new_from_array(created_by),
intent,
}
},
)
}
}
#[cfg(test)]
mod tests {
use core::mem::size_of;
use super::fixtures::{sample_account, CANCELLED_OFFSET, DISCRIMINATOR_OFFSET, INTENT_OFFSET};
use super::*;
use crate::data::intent::fixtures::{sample_intent, FLAGS_OFFSET};
use crate::data::intent::Flags;
#[test]
fn widths_match_field_sizes() {
use core::mem::size_of_val;
let OrderAccount {
bump,
cancelled,
amount_withdrawn,
amount_received,
created_by,
intent: _intent,
} = sample_account(false);
assert_eq!(EncodedOrderAccount::W_BUMP, size_of_val(&bump));
assert_eq!(EncodedOrderAccount::W_CANCELLED, size_of_val(&cancelled));
assert_eq!(
EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
size_of_val(&amount_withdrawn)
);
assert_eq!(
EncodedOrderAccount::W_AMOUNT_RECEIVED,
size_of_val(&amount_received)
);
assert_eq!(EncodedOrderAccount::W_CREATED_BY, size_of_val(&created_by));
assert_eq!(
EncodedOrderAccount::W_INTENT,
size_of::<EncodedOrderIntent>()
);
}
#[test]
fn roundtrip_both_cancelled_states() {
for cancelled in [false, true] {
let account = sample_account(cancelled);
let encoded = EncodedOrderAccount::from(account.clone());
let decoded = OrderAccount::try_from(encoded).expect("example must decode");
assert_eq!(decoded, account);
}
}
#[test]
fn is_fully_filled_tracks_the_exact_side_only() {
const SELL_AMOUNT: u64 = 1_000;
const BUY_AMOUNT: u64 = 2_000;
let account = |kind, amount_withdrawn, amount_received| OrderAccount {
amount_withdrawn,
amount_received,
intent: OrderIntent {
sell_amount: SELL_AMOUNT,
buy_amount: BUY_AMOUNT,
..sample_intent(Flags {
kind,
..Default::default()
})
},
..sample_account(false)
};
let cases = [
(OrderKind::Sell, SELL_AMOUNT, 0, true), (OrderKind::Buy, 0, BUY_AMOUNT, true), (OrderKind::Sell, u64::MAX, 0, true), (OrderKind::Sell, u64::MAX, u64::MAX, true), (OrderKind::Buy, 0, u64::MAX, true), (OrderKind::Buy, u64::MAX, u64::MAX, true), (OrderKind::Sell, 0, 0, false), (OrderKind::Sell, SELL_AMOUNT - 1, BUY_AMOUNT, false), (OrderKind::Buy, SELL_AMOUNT, BUY_AMOUNT - 1, false), ];
for (kind, withdrawn, received, expected) in cases {
assert_eq!(
account(kind, withdrawn, received).is_fully_filled(),
expected,
"{kind:?} order withdrawn={withdrawn} received={received}",
);
}
}
#[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 mut sample_account_base = sample_account(false);
let base: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account_base.clone()).into();
let cancelled: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(true)).into();
assert_eq!(
first_differing_byte(&base, &cancelled).expect("should differ in the cancelled byte"),
CANCELLED_OFFSET
);
let encoded_intent: [u8; EncodedOrderIntent::SIZE] =
(&EncodedOrderIntent::from(&sample_account_base.intent)).into();
let bitwise_different_encoded_intent: [u8; EncodedOrderIntent::SIZE] =
encoded_intent.map(|b| b ^ 0x01);
sample_account_base.intent =
OrderIntent::try_from(&bitwise_different_encoded_intent).expect("hack should work");
let changed_intent: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account_base).into();
assert_eq!(
first_differing_byte(&base, &changed_intent).expect("should differ in the intent slot"),
INTENT_OFFSET
);
}
#[test]
fn decode_rejects_wrong_discriminator() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(false)).into();
bytes[0] ^= 0xff;
let err = OrderAccount::try_from(bytes).expect_err("wrong discriminator must be rejected");
assert_eq!(err, ProgramError::InvalidAccountData);
}
#[test]
fn decode_rejects_non_boolean_cancelled() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(false)).into();
for bad in 0x02u8..=0xff {
bytes[CANCELLED_OFFSET] = bad;
let err = OrderAccount::try_from(bytes)
.expect_err("non-boolean cancelled byte must be rejected");
assert_eq!(err, ProgramError::InvalidAccountData);
}
}
#[test]
fn decode_propagates_invalid_intent() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(false)).into();
bytes[INTENT_OFFSET + FLAGS_OFFSET] = 0xff;
let err = OrderAccount::try_from(bytes)
.expect_err("an invalid intent flags byte must propagate as a decode failure");
assert_eq!(err, ProgramError::InvalidAccountData);
}
#[test]
fn decode_and_hash_catches_errors() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(false)).into();
bytes[CANCELLED_OFFSET] = 0xff;
let err = EncodedOrderAccount::decode_and_hash(&bytes)
.expect_err("decode_and_hash must propagate the try_from error");
assert_eq!(err, ProgramError::InvalidAccountData);
}
#[test]
fn try_from_exact_length_slice_decodes() {
let account = sample_account(false);
let bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(account.clone()).into();
let decoded = OrderAccount::try_from(&bytes[..]).expect("exact-length slice must decode");
assert_eq!(decoded, account);
}
#[test]
fn try_from_wrong_length_slice_is_rejected() {
let bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(false)).into();
assert_eq!(
OrderAccount::try_from(&bytes[..EncodedOrderAccount::SIZE - 1]),
Err(ProgramError::InvalidAccountData),
);
let too_long = [bytes.as_slice(), [0].as_slice()].concat();
assert_eq!(
OrderAccount::try_from(&too_long[..]),
Err(ProgramError::InvalidAccountData),
);
}
#[test]
fn try_from_slice_forwards_decoding_errors() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(false)).into();
bytes[CANCELLED_OFFSET] = 0xff;
assert_eq!(
OrderAccount::try_from(&bytes[..]),
Err(ProgramError::InvalidAccountData),
);
}
mod load_from_pda {
use super::*;
use crate::instruction::fixtures::fake_account_with_data;
use crate::pda::order::find_order_pda;
const PROGRAM_ID: Address = Address::new_from_array([0xc0; 32]);
fn canonical_account(cancelled: bool) -> (OrderAccount, Address) {
let mut account = sample_account(cancelled);
let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
account.bump = bump;
(account, pda_address)
}
#[test]
fn accepts_the_canonical_pda() {
let (account, pda_address) = canonical_account(false);
let order_pda = fake_account_with_data(
pda_address,
&EncodedOrderAccount::from(account.clone())[..],
);
let loaded = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
.expect("canonical PDA must load");
assert_eq!(loaded, account);
}
#[test]
fn rejects_a_non_canonical_address() {
let (account, _) = canonical_account(false);
let wrong_address = Pubkey::new_from_array([0x42; 32]);
let order_pda =
fake_account_with_data(wrong_address, &EncodedOrderAccount::from(account)[..]);
let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
.expect_err("a non-canonical address must be rejected");
assert_eq!(err, SettlementError::AccountNotDerivable.into());
}
#[test]
fn rejects_a_stored_bump_that_does_not_derive_the_address() {
let (mut account, pda_address) = canonical_account(false);
account.bump = account.bump.wrapping_sub(1);
let order_pda =
fake_account_with_data(pda_address, &EncodedOrderAccount::from(account)[..]);
let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
.expect_err("a non-canonical bump must be rejected");
assert_eq!(err, SettlementError::AccountNotDerivable.into());
}
#[test]
fn propagates_decode_errors() {
let (account, pda_address) = canonical_account(false);
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(account).into();
bytes[CANCELLED_OFFSET] = 0xff;
let order_pda = fake_account_with_data(pda_address, &bytes);
let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
.expect_err("a corrupt account must fail to decode");
assert_eq!(err, ProgramError::InvalidAccountData);
}
}
#[test]
fn direct_write_account_matches_order_account_decoding() {
let bump = 254;
let cancelled = true;
let amount_withdrawn = 1337;
let amount_received = 31337;
let intent = sample_intent(Default::default());
let created_by = Pubkey::new_from_array([0x42u8; 32]);
let mut buffer = [0u8; EncodedOrderAccount::SIZE];
write_account(
&mut buffer,
bump,
cancelled,
amount_withdrawn,
amount_received,
&created_by,
&<[u8; EncodedOrderIntent::SIZE]>::from(&EncodedOrderIntent::from(&intent)),
);
let direct = EncodedOrderAccount(buffer);
let via_order_account = EncodedOrderAccount::from(OrderAccount {
bump,
cancelled,
amount_withdrawn,
amount_received,
created_by,
intent,
});
assert_eq!(direct, via_order_account);
}
mod proptest {
use ::proptest::{prelude::*, test_runner::TestCaseError};
use super::*;
use crate::data::{intent::fixtures::arb_flags_byte, order::fixtures::arb_order_account};
proptest! {
#[test]
fn account_roundtrip(account in arb_order_account()) {
let encoded = EncodedOrderAccount::from(account.clone());
let decoded = OrderAccount::try_from(encoded)
.map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
prop_assert_eq!(decoded, account);
}
#[test]
fn bytes_roundtrip(
mut bytes in any::<[u8; EncodedOrderAccount::SIZE]>(),
cancelled in any::<bool>(),
flags in arb_flags_byte(),
) {
bytes[DISCRIMINATOR_OFFSET] = EncodedOrderAccount::DISCRIMINATOR;
bytes[CANCELLED_OFFSET] = cancelled as u8;
bytes[INTENT_OFFSET + FLAGS_OFFSET] = flags;
let account = OrderAccount::try_from(bytes)
.map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
prop_assert_eq!(*EncodedOrderAccount::from(account), bytes);
}
#[test]
fn consistent_decode_and_hash(account in arb_order_account()) {
let encoded = EncodedOrderAccount::from(account.clone());
let (decoded, hash) = EncodedOrderAccount::decode_and_hash(&encoded)
.map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
prop_assert_eq!(hash, account.intent.uid());
prop_assert_eq!(decoded, account);
}
}
}
}