1use solana_program_error::{ProgramError, ProgramResult};
6use solana_pubkey::Pubkey;
7use wincode::{deserialize, SchemaRead, SchemaWrite};
8
9use crate::{error::RoshiError, oracle::OracleConfig, state::ASSET_ACCOUNT_TAG, ID};
10
11const FLAG_FALSE: u8 = 0;
12const FLAG_TRUE: u8 = 1;
13
14const fn flag(value: bool) -> u8 {
15 value as u8
16}
17
18fn bool_flag(flag: u8) -> Result<bool, ProgramError> {
19 match flag {
20 FLAG_FALSE => Ok(false),
21 FLAG_TRUE => Ok(true),
22 _ => Err(RoshiError::InvalidAssetAccount.into()),
23 }
24}
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq, SchemaWrite, SchemaRead)]
27#[wincode(assert_zero_copy)]
28#[repr(C)]
29pub struct Asset {
30 pub vault: [u8; 32],
32 pub asset_mint: [u8; 32],
34 pub oracle: OracleConfig,
38 pub deposit_cap_atoms: u64,
42 pub asset_decimals: u8,
44 enabled_flag: u8,
46 routed_flag: u8,
50 pub bump: u8,
51 _padding: [u8; 4],
52}
53
54impl Asset {
55 pub const SEED: &'static [u8] = b"asset";
56 pub const SPACE: usize = std::mem::size_of::<Self>() + 1;
57
58 #[allow(clippy::too_many_arguments)]
59 pub fn new(
60 vault: [u8; 32],
61 asset_mint: [u8; 32],
62 oracle: OracleConfig,
63 asset_decimals: u8,
64 enabled: bool,
65 routed: bool,
66 deposit_cap_atoms: u64,
67 bump: u8,
68 ) -> Result<Self, ProgramError> {
69 oracle
70 .validate()
71 .map_err(|_| ProgramError::from(RoshiError::InvalidAssetAccount))?;
72
73 Ok(Self {
74 vault,
75 asset_mint,
76 oracle,
77 deposit_cap_atoms,
78 asset_decimals,
79 enabled_flag: flag(enabled),
80 routed_flag: flag(routed),
81 bump,
82 _padding: [0; 4],
83 })
84 }
85
86 pub fn find_address(vault: &Pubkey, asset_mint: &Pubkey) -> (Pubkey, u8) {
87 Pubkey::find_program_address(&[Self::SEED, vault.as_ref(), asset_mint.as_ref()], &ID)
88 }
89
90 pub fn from_account_data(data: &[u8]) -> Result<Self, ProgramError> {
93 let (&tag, rest) = data
94 .split_first()
95 .ok_or(ProgramError::from(RoshiError::InvalidAssetAccount))?;
96 if tag != ASSET_ACCOUNT_TAG {
97 return Err(RoshiError::InvalidAssetAccount.into());
98 }
99 let asset: Self =
100 deserialize(rest).map_err(|_| ProgramError::from(RoshiError::InvalidAssetAccount))?;
101 asset.validate_state()?;
102 Ok(asset)
103 }
104
105 pub fn enabled(&self) -> Result<bool, ProgramError> {
106 bool_flag(self.enabled_flag)
107 }
108
109 pub fn set_enabled(&mut self, enabled: bool) {
110 self.enabled_flag = flag(enabled);
111 }
112
113 pub fn routed(&self) -> Result<bool, ProgramError> {
114 bool_flag(self.routed_flag)
115 }
116
117 pub fn set_routed(&mut self, routed: bool) {
118 self.routed_flag = flag(routed);
119 }
120
121 pub fn validate_state(&self) -> ProgramResult {
122 self.oracle
123 .validate()
124 .map_err(|_| ProgramError::from(RoshiError::InvalidAssetAccount))?;
125 bool_flag(self.enabled_flag)?;
126 bool_flag(self.routed_flag)?;
127 Ok(())
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use wincode::{config::DefaultConfig, serialize, TypeMeta};
135
136 fn assert_zero_copy<T>()
137 where
138 T: wincode::ZeroCopy,
139 T: for<'de> SchemaRead<'de, DefaultConfig> + SchemaWrite<DefaultConfig>,
140 {
141 assert_eq!(
142 <T as SchemaRead<'_, DefaultConfig>>::TYPE_META,
143 TypeMeta::Static {
144 size: core::mem::size_of::<T>(),
145 zero_copy: true,
146 }
147 );
148 assert_eq!(
149 <T as SchemaWrite<DefaultConfig>>::TYPE_META,
150 TypeMeta::Static {
151 size: core::mem::size_of::<T>(),
152 zero_copy: true,
153 }
154 );
155 }
156
157 fn test_asset(enabled: bool, routed: bool) -> Asset {
158 Asset::new(
159 [1; 32],
160 [2; 32],
161 OracleConfig::default(),
162 6,
163 enabled,
164 routed,
165 u64::MAX,
166 7,
167 )
168 .unwrap()
169 }
170
171 fn account_data(asset: &Asset) -> Vec<u8> {
173 let mut data = vec![ASSET_ACCOUNT_TAG];
174 data.extend_from_slice(&serialize(asset).unwrap());
175 data
176 }
177
178 fn routed_flag_offset() -> usize {
181 1 + core::mem::size_of::<[u8; 32]>()
182 + core::mem::size_of::<[u8; 32]>()
183 + core::mem::size_of::<OracleConfig>()
184 + core::mem::size_of::<u64>()
185 + core::mem::size_of::<u8>()
186 + core::mem::size_of::<u8>()
187 }
188
189 #[test]
190 fn asset_is_zero_copy_with_explicit_padding() {
191 let asset = test_asset(true, false);
192 assert_zero_copy::<Asset>();
193 assert_eq!(core::mem::size_of::<Asset>(), 280);
194 assert_eq!(Asset::SPACE, 281);
195 assert_eq!(
196 serialize(&asset).unwrap().len(),
197 core::mem::size_of::<Asset>()
198 );
199 }
200
201 #[test]
202 fn enabled_and_routed_flags_use_typed_accessors() {
203 let mut asset = test_asset(false, false);
204 assert_eq!(asset.enabled(), Ok(false));
205 asset.set_enabled(true);
206 assert_eq!(asset.enabled(), Ok(true));
207 assert_eq!(asset.routed(), Ok(false));
208 asset.set_routed(true);
209 assert_eq!(asset.routed(), Ok(true));
210 }
211
212 #[test]
213 fn from_account_data_round_trips_a_valid_asset() {
214 let asset = test_asset(true, true);
215 let decoded = Asset::from_account_data(&account_data(&asset)).unwrap();
216 assert_eq!(decoded, asset);
217 assert_eq!(decoded.routed(), Ok(true));
218 }
219
220 #[test]
221 fn from_account_data_rejects_a_bad_tag() {
222 let mut data = account_data(&test_asset(true, false));
223 data[0] = ASSET_ACCOUNT_TAG.wrapping_add(1);
224 assert_eq!(
225 Asset::from_account_data(&data),
226 Err(ProgramError::from(RoshiError::InvalidAssetAccount))
227 );
228 }
229
230 #[test]
231 fn from_account_data_rejects_an_invalid_routed_flag() {
232 let mut data = account_data(&test_asset(true, false));
233 data[routed_flag_offset()] = 255;
234 assert_eq!(
235 Asset::from_account_data(&data),
236 Err(ProgramError::from(RoshiError::InvalidAssetAccount))
237 );
238 }
239}