use solana_program_error::{ProgramError, ProgramResult};
use solana_pubkey::Pubkey;
use wincode::{deserialize, SchemaRead, SchemaWrite};
use crate::{error::RoshiError, oracle::OracleConfig, state::ASSET_ACCOUNT_TAG, ID};
const FLAG_FALSE: u8 = 0;
const FLAG_TRUE: u8 = 1;
const fn flag(value: bool) -> u8 {
value as u8
}
fn bool_flag(flag: u8) -> Result<bool, ProgramError> {
match flag {
FLAG_FALSE => Ok(false),
FLAG_TRUE => Ok(true),
_ => Err(RoshiError::InvalidAssetAccount.into()),
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, SchemaWrite, SchemaRead)]
#[wincode(assert_zero_copy)]
#[repr(C)]
pub struct Asset {
pub vault: [u8; 32],
pub asset_mint: [u8; 32],
pub oracle: OracleConfig,
pub deposit_cap_atoms: u64,
pub asset_decimals: u8,
enabled_flag: u8,
routed_flag: u8,
pub bump: u8,
_padding: [u8; 4],
}
impl Asset {
pub const SEED: &'static [u8] = b"asset";
pub const SPACE: usize = std::mem::size_of::<Self>() + 1;
#[allow(clippy::too_many_arguments)]
pub fn new(
vault: [u8; 32],
asset_mint: [u8; 32],
oracle: OracleConfig,
asset_decimals: u8,
enabled: bool,
routed: bool,
deposit_cap_atoms: u64,
bump: u8,
) -> Result<Self, ProgramError> {
oracle
.validate()
.map_err(|_| ProgramError::from(RoshiError::InvalidAssetAccount))?;
Ok(Self {
vault,
asset_mint,
oracle,
deposit_cap_atoms,
asset_decimals,
enabled_flag: flag(enabled),
routed_flag: flag(routed),
bump,
_padding: [0; 4],
})
}
pub fn find_address(vault: &Pubkey, asset_mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[Self::SEED, vault.as_ref(), asset_mint.as_ref()], &ID)
}
pub fn from_account_data(data: &[u8]) -> Result<Self, ProgramError> {
let (&tag, rest) = data
.split_first()
.ok_or(ProgramError::from(RoshiError::InvalidAssetAccount))?;
if tag != ASSET_ACCOUNT_TAG {
return Err(RoshiError::InvalidAssetAccount.into());
}
let asset: Self =
deserialize(rest).map_err(|_| ProgramError::from(RoshiError::InvalidAssetAccount))?;
asset.validate_state()?;
Ok(asset)
}
pub fn enabled(&self) -> Result<bool, ProgramError> {
bool_flag(self.enabled_flag)
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled_flag = flag(enabled);
}
pub fn routed(&self) -> Result<bool, ProgramError> {
bool_flag(self.routed_flag)
}
pub fn set_routed(&mut self, routed: bool) {
self.routed_flag = flag(routed);
}
pub fn validate_state(&self) -> ProgramResult {
self.oracle
.validate()
.map_err(|_| ProgramError::from(RoshiError::InvalidAssetAccount))?;
bool_flag(self.enabled_flag)?;
bool_flag(self.routed_flag)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use wincode::{config::DefaultConfig, serialize, TypeMeta};
fn assert_zero_copy<T>()
where
T: wincode::ZeroCopy,
T: for<'de> SchemaRead<'de, DefaultConfig> + SchemaWrite<DefaultConfig>,
{
assert_eq!(
<T as SchemaRead<'_, DefaultConfig>>::TYPE_META,
TypeMeta::Static {
size: core::mem::size_of::<T>(),
zero_copy: true,
}
);
assert_eq!(
<T as SchemaWrite<DefaultConfig>>::TYPE_META,
TypeMeta::Static {
size: core::mem::size_of::<T>(),
zero_copy: true,
}
);
}
fn test_asset(enabled: bool, routed: bool) -> Asset {
Asset::new(
[1; 32],
[2; 32],
OracleConfig::default(),
6,
enabled,
routed,
u64::MAX,
7,
)
.unwrap()
}
fn account_data(asset: &Asset) -> Vec<u8> {
let mut data = vec![ASSET_ACCOUNT_TAG];
data.extend_from_slice(&serialize(asset).unwrap());
data
}
fn routed_flag_offset() -> usize {
1 + core::mem::size_of::<[u8; 32]>()
+ core::mem::size_of::<[u8; 32]>()
+ core::mem::size_of::<OracleConfig>()
+ core::mem::size_of::<u64>()
+ core::mem::size_of::<u8>()
+ core::mem::size_of::<u8>()
}
#[test]
fn asset_is_zero_copy_with_explicit_padding() {
let asset = test_asset(true, false);
assert_zero_copy::<Asset>();
assert_eq!(core::mem::size_of::<Asset>(), 280);
assert_eq!(Asset::SPACE, 281);
assert_eq!(
serialize(&asset).unwrap().len(),
core::mem::size_of::<Asset>()
);
}
#[test]
fn enabled_and_routed_flags_use_typed_accessors() {
let mut asset = test_asset(false, false);
assert_eq!(asset.enabled(), Ok(false));
asset.set_enabled(true);
assert_eq!(asset.enabled(), Ok(true));
assert_eq!(asset.routed(), Ok(false));
asset.set_routed(true);
assert_eq!(asset.routed(), Ok(true));
}
#[test]
fn from_account_data_round_trips_a_valid_asset() {
let asset = test_asset(true, true);
let decoded = Asset::from_account_data(&account_data(&asset)).unwrap();
assert_eq!(decoded, asset);
assert_eq!(decoded.routed(), Ok(true));
}
#[test]
fn from_account_data_rejects_a_bad_tag() {
let mut data = account_data(&test_asset(true, false));
data[0] = ASSET_ACCOUNT_TAG.wrapping_add(1);
assert_eq!(
Asset::from_account_data(&data),
Err(ProgramError::from(RoshiError::InvalidAssetAccount))
);
}
#[test]
fn from_account_data_rejects_an_invalid_routed_flag() {
let mut data = account_data(&test_asset(true, false));
data[routed_flag_offset()] = 255;
assert_eq!(
Asset::from_account_data(&data),
Err(ProgramError::from(RoshiError::InvalidAssetAccount))
);
}
}