1use alloc::string::ToString;
2use core::fmt;
3
4use super::vault::AssetVaultKey;
5use super::{Asset, AssetAmount, AssetCallbackFlag, AssetComposition, 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: AssetAmount,
30 callbacks: AssetCallbackFlag,
31}
32
33impl FungibleAsset {
34 pub const MAX_AMOUNT: AssetAmount = AssetAmount::MAX;
41
42 pub const SERIALIZED_SIZE: usize = AssetComposition::SERIALIZED_SIZE
47 + AccountId::SERIALIZED_SIZE
48 + core::mem::size_of::<u64>()
49 + AssetCallbackFlag::SERIALIZED_SIZE;
50
51 pub fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
61 let amount = AssetAmount::new(amount)?;
63
64 Ok(Self {
65 faucet_id,
66 amount,
67 callbacks: AssetCallbackFlag::default(),
68 })
69 }
70
71 pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
82 if !key.composition().is_fungible() {
83 return Err(AssetError::AssetCompositionMismatch {
84 faucet_id: key.faucet_id(),
85 expected: AssetComposition::Fungible,
86 actual: key.composition(),
87 });
88 }
89
90 if !key.asset_id().is_empty() {
91 return Err(AssetError::FungibleAssetIdMustBeZero(key.asset_id()));
92 }
93
94 if value[1] != Felt::ZERO || value[2] != Felt::ZERO || value[3] != Felt::ZERO {
95 return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value));
96 }
97
98 let mut asset = Self::new(key.faucet_id(), value[0].as_canonical_u64())?;
99 asset.callbacks = key.callback_flag();
100
101 Ok(asset)
102 }
103
104 pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
113 let vault_key = AssetVaultKey::try_from(key)?;
114 Self::from_key_value(vault_key, value)
115 }
116
117 pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self {
119 self.callbacks = callbacks;
120 self
121 }
122
123 pub fn faucet_id(&self) -> AccountId {
128 self.faucet_id
129 }
130
131 pub fn amount(&self) -> AssetAmount {
133 self.amount
134 }
135
136 pub fn is_same(&self, other: &Self) -> bool {
138 self.vault_key() == other.vault_key()
139 }
140
141 pub fn callbacks(&self) -> AssetCallbackFlag {
143 self.callbacks
144 }
145
146 pub fn vault_key(&self) -> AssetVaultKey {
148 AssetVaultKey::new(
149 AssetId::default(),
150 self.faucet_id,
151 AssetComposition::Fungible,
152 self.callbacks,
153 )
154 .expect("default asset id should be valid for fungible composition")
155 }
156
157 pub fn to_key_word(&self) -> Word {
159 self.vault_key().to_word()
160 }
161
162 pub fn to_value_word(&self) -> Word {
164 Word::new([Felt::from(self.amount), Felt::ZERO, Felt::ZERO, Felt::ZERO])
165 }
166
167 #[allow(clippy::should_implement_trait)]
177 pub fn add(self, other: Self) -> Result<Self, AssetError> {
178 if !self.is_same(&other) {
179 return Err(AssetError::FungibleAssetInconsistentVaultKeys {
180 original_key: self.vault_key(),
181 other_key: other.vault_key(),
182 });
183 }
184
185 let amount = (self.amount + other.amount)?;
186
187 Ok(Self {
188 faucet_id: self.faucet_id,
189 amount,
190 callbacks: self.callbacks,
191 })
192 }
193
194 #[allow(clippy::should_implement_trait)]
201 pub fn sub(self, other: Self) -> Result<Self, AssetError> {
202 if !self.is_same(&other) {
203 return Err(AssetError::FungibleAssetInconsistentVaultKeys {
204 original_key: self.vault_key(),
205 other_key: other.vault_key(),
206 });
207 }
208
209 let amount = (self.amount - other.amount)?;
210
211 Ok(FungibleAsset {
212 faucet_id: self.faucet_id,
213 amount,
214 callbacks: self.callbacks,
215 })
216 }
217}
218
219impl From<FungibleAsset> for Asset {
220 fn from(asset: FungibleAsset) -> Self {
221 Asset::Fungible(asset)
222 }
223}
224
225impl fmt::Display for FungibleAsset {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 write!(f, "{self:?}")
229 }
230}
231
232impl Serializable for FungibleAsset {
236 fn write_into<W: ByteWriter>(&self, target: &mut W) {
237 target.write(AssetComposition::Fungible);
239 target.write(self.faucet_id);
240 target.write(self.amount.as_u64());
241 target.write(self.callbacks);
242 }
243
244 fn get_size_hint(&self) -> usize {
245 AssetComposition::SERIALIZED_SIZE
246 + self.faucet_id.get_size_hint()
247 + self.amount.as_u64().get_size_hint()
248 + self.callbacks.get_size_hint()
249 }
250}
251
252impl Deserializable for FungibleAsset {
253 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
254 let composition: AssetComposition = source.read()?;
255 if !composition.is_fungible() {
256 return Err(DeserializationError::InvalidValue(format!(
257 "expected fungible asset composition but found {composition:?}"
258 )));
259 }
260 FungibleAsset::deserialize_body(source)
261 }
262}
263
264impl FungibleAsset {
265 pub(super) fn deserialize_body<R: ByteReader>(
268 source: &mut R,
269 ) -> Result<Self, DeserializationError> {
270 let faucet_id: AccountId = source.read()?;
271 let amount: u64 = source.read()?;
272 let callbacks = source.read()?;
273
274 let asset = FungibleAsset::new(faucet_id, amount)
275 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?
276 .with_callbacks(callbacks);
277
278 Ok(asset)
279 }
280}
281
282#[cfg(test)]
286mod tests {
287 use assert_matches::assert_matches;
288
289 use super::*;
290 use crate::account::AccountId;
291 use crate::asset::NonFungibleAsset;
292 use crate::asset::tests::set_asset_metadata;
293 use crate::testing::account_id::{
294 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
295 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
296 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
297 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
298 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
299 };
300
301 #[test]
302 fn fungible_asset_from_key_value_words_fails_on_invalid_composition() -> anyhow::Result<()> {
303 let asset_key =
304 set_asset_metadata(FungibleAsset::mock(25).vault_key(), AssetComposition::None.as_u8());
305
306 let err =
307 FungibleAsset::from_key_value_words(asset_key, FungibleAsset::mock(5).to_value_word())
308 .unwrap_err();
309 assert_matches!(err, AssetError::AssetCompositionMismatch {
310 faucet_id: _, expected, actual: _
311 } => {
312 assert_eq!(expected, AssetComposition::Fungible);
313 });
314
315 Ok(())
316 }
317
318 #[test]
319 fn fungible_asset_from_key_value_words_fails_on_invalid_asset_id() -> anyhow::Result<()> {
320 let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
321 let mut asset_key = AssetVaultKey::new(
322 AssetId::default(),
323 faucet_id,
324 AssetComposition::Fungible,
325 AssetCallbackFlag::Disabled,
326 )?
327 .to_word();
328 asset_key[0] = Felt::from(1u32);
329 asset_key[1] = Felt::from(2u32);
330
331 let err =
332 FungibleAsset::from_key_value_words(asset_key, FungibleAsset::mock(5).to_value_word())
333 .unwrap_err();
334 assert_matches!(err, AssetError::FungibleAssetIdMustBeZero(_));
335
336 Ok(())
337 }
338
339 #[test]
340 fn fungible_asset_from_key_value_fails_on_invalid_value() -> anyhow::Result<()> {
341 let asset = FungibleAsset::mock(42);
342 let mut invalid_value = asset.to_value_word();
343 invalid_value[2] = Felt::from(5u32);
344
345 let err = FungibleAsset::from_key_value(asset.vault_key(), invalid_value).unwrap_err();
346 assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
347
348 Ok(())
349 }
350
351 #[test]
352 fn test_fungible_asset_serde() -> anyhow::Result<()> {
353 for fungible_account_id in [
354 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
355 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
356 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
357 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
358 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
359 ] {
360 let account_id = AccountId::try_from(fungible_account_id).unwrap();
361 let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
362 assert_eq!(
363 fungible_asset,
364 FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
365 );
366 assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint());
367
368 assert_eq!(
369 fungible_asset,
370 FungibleAsset::from_key_value_words(
371 fungible_asset.to_key_word(),
372 fungible_asset.to_value_word()
373 )?
374 )
375 }
376
377 let non_fungible_asset = NonFungibleAsset::mock(&[4]);
378 let err = FungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap_err();
379 assert_matches!(err, DeserializationError::InvalidValue(msg) => {
380 assert!(msg.contains("expected fungible asset composition but found None"));
381 });
382
383 Ok(())
384 }
385
386 #[test]
387 fn test_vault_key_for_fungible_asset() {
388 let asset = FungibleAsset::mock(34);
389
390 assert_eq!(asset.vault_key().faucet_id(), FungibleAsset::mock_issuer());
391 assert_eq!(asset.vault_key().asset_id().prefix().as_canonical_u64(), 0);
392 assert_eq!(asset.vault_key().asset_id().suffix().as_canonical_u64(), 0);
393 }
394}