1use alloc::string::ToString;
2use core::fmt;
3
4use super::vault::AssetVaultKey;
5use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word};
6use crate::Felt;
7use crate::account::AccountId;
8use crate::asset::AssetId;
9use crate::utils::serde::{
10 ByteReader,
11 ByteWriter,
12 Deserializable,
13 DeserializationError,
14 Serializable,
15};
16
17#[derive(Debug, Copy, Clone, PartialEq, Eq)]
27pub struct FungibleAsset {
28 faucet_id: AccountId,
29 amount: u64,
30 callbacks: AssetCallbackFlag,
31}
32
33impl FungibleAsset {
34 pub const MAX_AMOUNT: u64 = 2u64.pow(63) - 2u64.pow(31);
41
42 pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE
46 + core::mem::size_of::<u64>()
47 + AssetCallbackFlag::SERIALIZED_SIZE;
48
49 pub fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
60 if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
61 return Err(AssetError::FungibleFaucetIdTypeMismatch(faucet_id));
62 }
63
64 if amount > Self::MAX_AMOUNT {
65 return Err(AssetError::FungibleAssetAmountTooBig(amount));
66 }
67
68 Ok(Self {
69 faucet_id,
70 amount,
71 callbacks: AssetCallbackFlag::default(),
72 })
73 }
74
75 pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
86 if !key.asset_id().is_empty() {
87 return Err(AssetError::FungibleAssetIdMustBeZero(key.asset_id()));
88 }
89
90 if value[1] != Felt::ZERO || value[2] != Felt::ZERO || value[3] != Felt::ZERO {
91 return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value));
92 }
93
94 let mut asset = Self::new(key.faucet_id(), value[0].as_canonical_u64())?;
95 asset.callbacks = key.callback_flag();
96
97 Ok(asset)
98 }
99
100 pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
110 let vault_key = AssetVaultKey::try_from(key)?;
111 Self::from_key_value(vault_key, value)
112 }
113
114 pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self {
116 self.callbacks = callbacks;
117 self
118 }
119
120 pub fn faucet_id(&self) -> AccountId {
125 self.faucet_id
126 }
127
128 pub fn amount(&self) -> u64 {
130 self.amount
131 }
132
133 pub fn is_same(&self, other: &Self) -> bool {
135 self.vault_key() == other.vault_key()
136 }
137
138 pub fn callbacks(&self) -> AssetCallbackFlag {
140 self.callbacks
141 }
142
143 pub fn vault_key(&self) -> AssetVaultKey {
145 AssetVaultKey::new(AssetId::default(), self.faucet_id, self.callbacks)
146 .expect("faucet ID should be of type fungible")
147 }
148
149 pub fn to_key_word(&self) -> Word {
151 self.vault_key().to_word()
152 }
153
154 pub fn to_value_word(&self) -> Word {
156 Word::new([
157 Felt::try_from(self.amount)
158 .expect("fungible asset should only allow amounts that fit into a felt"),
159 Felt::ZERO,
160 Felt::ZERO,
161 Felt::ZERO,
162 ])
163 }
164
165 #[allow(clippy::should_implement_trait)]
175 pub fn add(self, other: Self) -> Result<Self, AssetError> {
176 if !self.is_same(&other) {
177 return Err(AssetError::FungibleAssetInconsistentVaultKeys {
178 original_key: self.vault_key(),
179 other_key: other.vault_key(),
180 });
181 }
182
183 let amount = self
184 .amount
185 .checked_add(other.amount)
186 .expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64");
187 if amount > Self::MAX_AMOUNT {
188 return Err(AssetError::FungibleAssetAmountTooBig(amount));
189 }
190
191 Ok(Self {
192 faucet_id: self.faucet_id,
193 amount,
194 callbacks: self.callbacks,
195 })
196 }
197
198 #[allow(clippy::should_implement_trait)]
205 pub fn sub(self, other: Self) -> Result<Self, AssetError> {
206 if !self.is_same(&other) {
207 return Err(AssetError::FungibleAssetInconsistentVaultKeys {
208 original_key: self.vault_key(),
209 other_key: other.vault_key(),
210 });
211 }
212
213 let amount = self.amount.checked_sub(other.amount).ok_or(
214 AssetError::FungibleAssetAmountNotSufficient {
215 minuend: self.amount,
216 subtrahend: other.amount,
217 },
218 )?;
219
220 Ok(FungibleAsset {
221 faucet_id: self.faucet_id,
222 amount,
223 callbacks: self.callbacks,
224 })
225 }
226}
227
228impl From<FungibleAsset> for Asset {
229 fn from(asset: FungibleAsset) -> Self {
230 Asset::Fungible(asset)
231 }
232}
233
234impl fmt::Display for FungibleAsset {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 write!(f, "{self:?}")
238 }
239}
240
241impl Serializable for FungibleAsset {
245 fn write_into<W: ByteWriter>(&self, target: &mut W) {
246 target.write(self.faucet_id);
249 target.write(self.amount);
250 target.write(self.callbacks);
251 }
252
253 fn get_size_hint(&self) -> usize {
254 self.faucet_id.get_size_hint()
255 + self.amount.get_size_hint()
256 + self.callbacks.get_size_hint()
257 }
258}
259
260impl Deserializable for FungibleAsset {
261 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
262 let faucet_id: AccountId = source.read()?;
263 FungibleAsset::deserialize_with_faucet_id(faucet_id, source)
264 }
265}
266
267impl FungibleAsset {
268 pub(super) fn deserialize_with_faucet_id<R: ByteReader>(
271 faucet_id: AccountId,
272 source: &mut R,
273 ) -> Result<Self, DeserializationError> {
274 let amount: u64 = source.read()?;
275 let callbacks = source.read()?;
276
277 let asset = FungibleAsset::new(faucet_id, amount)
278 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?
279 .with_callbacks(callbacks);
280
281 Ok(asset)
282 }
283}
284
285#[cfg(test)]
289mod tests {
290 use assert_matches::assert_matches;
291
292 use super::*;
293 use crate::account::AccountId;
294 use crate::testing::account_id::{
295 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
296 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
297 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
298 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
299 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
300 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
301 };
302
303 #[test]
304 fn fungible_asset_from_key_value_words_fails_on_invalid_asset_id() -> anyhow::Result<()> {
305 let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
306 let invalid_key = Word::from([
307 Felt::from(1u32),
308 Felt::from(2u32),
309 faucet_id.suffix(),
310 faucet_id.prefix().as_felt(),
311 ]);
312
313 let err = FungibleAsset::from_key_value_words(
314 invalid_key,
315 FungibleAsset::mock(5).to_value_word(),
316 )
317 .unwrap_err();
318 assert_matches!(err, AssetError::FungibleAssetIdMustBeZero(_));
319
320 Ok(())
321 }
322
323 #[test]
324 fn fungible_asset_from_key_value_fails_on_invalid_value() -> anyhow::Result<()> {
325 let asset = FungibleAsset::mock(42);
326 let mut invalid_value = asset.to_value_word();
327 invalid_value[2] = Felt::from(5u32);
328
329 let err = FungibleAsset::from_key_value(asset.vault_key(), invalid_value).unwrap_err();
330 assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
331
332 Ok(())
333 }
334
335 #[test]
336 fn test_fungible_asset_serde() -> anyhow::Result<()> {
337 for fungible_account_id in [
338 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
339 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
340 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
341 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
342 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
343 ] {
344 let account_id = AccountId::try_from(fungible_account_id).unwrap();
345 let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
346 assert_eq!(
347 fungible_asset,
348 FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
349 );
350 assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint());
351
352 assert_eq!(
353 fungible_asset,
354 FungibleAsset::from_key_value_words(
355 fungible_asset.to_key_word(),
356 fungible_asset.to_value_word()
357 )?
358 )
359 }
360
361 let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap();
362 let asset = FungibleAsset::new(account_id, 50).unwrap();
363 let mut asset_bytes = asset.to_bytes();
364 assert_eq!(asset_bytes.len(), asset.get_size_hint());
365 assert_eq!(asset.get_size_hint(), FungibleAsset::SERIALIZED_SIZE);
366
367 let non_fungible_faucet_id =
368 AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
369
370 asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes());
372 let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
373 assert!(matches!(err, DeserializationError::InvalidValue(_)));
374
375 Ok(())
376 }
377
378 #[test]
379 fn test_vault_key_for_fungible_asset() {
380 let asset = FungibleAsset::mock(34);
381
382 assert_eq!(asset.vault_key().faucet_id(), FungibleAsset::mock_issuer());
383 assert_eq!(asset.vault_key().asset_id().prefix().as_canonical_u64(), 0);
384 assert_eq!(asset.vault_key().asset_id().suffix().as_canonical_u64(), 0);
385 }
386}