miden_protocol/asset/
fungible.rs1use alloc::string::ToString;
2use core::fmt;
3
4use super::vault::AssetId;
5use super::{Asset, AssetAmount, AssetComposition, AssetError, Word};
6use crate::Felt;
7use crate::account::{AccountId, AssetCallbackFlag};
8use crate::asset::AssetClass;
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}
31
32impl FungibleAsset {
33 pub const MAX_AMOUNT: AssetAmount = AssetAmount::MAX;
40
41 pub const SERIALIZED_SIZE: usize = AssetComposition::SERIALIZED_SIZE
45 + AccountId::SERIALIZED_SIZE
46 + core::mem::size_of::<u64>();
47
48 pub fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
58 let amount = AssetAmount::new(amount)?;
60
61 Ok(Self { faucet_id, amount })
62 }
63
64 pub fn from_id_and_value(id: AssetId, value: Word) -> Result<Self, AssetError> {
75 if !id.composition().is_fungible() {
76 return Err(AssetError::AssetCompositionMismatch {
77 faucet_id: id.faucet_id(),
78 expected: AssetComposition::Fungible,
79 actual: id.composition(),
80 });
81 }
82
83 if !id.asset_class().is_empty() {
84 return Err(AssetError::FungibleAssetClassMustBeZero(id.asset_class()));
85 }
86
87 if value[1] != Felt::ZERO || value[2] != Felt::ZERO || value[3] != Felt::ZERO {
88 return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value));
89 }
90
91 Self::new(id.faucet_id(), value[0].as_canonical_u64())
92 }
93
94 pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
103 let asset_id = AssetId::try_from(id)?;
104 Self::from_id_and_value(asset_id, value)
105 }
106
107 pub fn faucet_id(&self) -> AccountId {
112 self.faucet_id
113 }
114
115 pub fn callbacks(&self) -> AssetCallbackFlag {
117 self.faucet_id.asset_callback_flag()
118 }
119
120 pub fn amount(&self) -> AssetAmount {
122 self.amount
123 }
124
125 pub fn is_same(&self, other: &Self) -> bool {
127 self.id() == other.id()
128 }
129
130 pub fn id(&self) -> AssetId {
132 AssetId::new(AssetClass::default(), self.faucet_id, AssetComposition::Fungible)
133 .expect("default asset class should be valid for fungible composition")
134 }
135
136 pub fn to_id_word(&self) -> Word {
138 self.id().to_word()
139 }
140
141 pub fn to_value_word(&self) -> Word {
143 Word::new([Felt::from(self.amount), Felt::ZERO, Felt::ZERO, Felt::ZERO])
144 }
145
146 #[allow(clippy::should_implement_trait)]
156 pub fn add(self, other: Self) -> Result<Self, AssetError> {
157 if !self.is_same(&other) {
158 return Err(AssetError::FungibleAssetInconsistentIds {
159 original_id: self.id(),
160 other_id: other.id(),
161 });
162 }
163
164 let amount = (self.amount + other.amount)?;
165
166 Ok(Self { faucet_id: self.faucet_id, amount })
167 }
168
169 #[allow(clippy::should_implement_trait)]
176 pub fn sub(self, other: Self) -> Result<Self, AssetError> {
177 if !self.is_same(&other) {
178 return Err(AssetError::FungibleAssetInconsistentIds {
179 original_id: self.id(),
180 other_id: other.id(),
181 });
182 }
183
184 let amount = (self.amount - other.amount)?;
185
186 Ok(FungibleAsset { faucet_id: self.faucet_id, amount })
187 }
188}
189
190impl From<FungibleAsset> for Asset {
191 fn from(asset: FungibleAsset) -> Self {
192 Asset::Fungible(asset)
193 }
194}
195
196impl fmt::Display for FungibleAsset {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 write!(f, "{self:?}")
200 }
201}
202
203impl Serializable for FungibleAsset {
207 fn write_into<W: ByteWriter>(&self, target: &mut W) {
208 target.write(AssetComposition::Fungible);
210 target.write(self.faucet_id);
211 target.write(self.amount.as_u64());
212 }
213
214 fn get_size_hint(&self) -> usize {
215 AssetComposition::SERIALIZED_SIZE
216 + self.faucet_id.get_size_hint()
217 + self.amount.as_u64().get_size_hint()
218 }
219}
220
221impl Deserializable for FungibleAsset {
222 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
223 let composition: AssetComposition = source.read()?;
224 if !composition.is_fungible() {
225 return Err(DeserializationError::InvalidValue(format!(
226 "expected fungible asset composition but found {composition:?}"
227 )));
228 }
229 FungibleAsset::deserialize_body(source)
230 }
231}
232
233impl FungibleAsset {
234 pub(super) fn deserialize_body<R: ByteReader>(
237 source: &mut R,
238 ) -> Result<Self, DeserializationError> {
239 let faucet_id: AccountId = source.read()?;
240 let amount: u64 = source.read()?;
241
242 FungibleAsset::new(faucet_id, amount)
243 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
244 }
245}
246
247#[cfg(test)]
251mod tests {
252 use assert_matches::assert_matches;
253
254 use super::*;
255 use crate::account::AccountId;
256 use crate::asset::NonFungibleAsset;
257 use crate::asset::tests::set_asset_metadata;
258 use crate::testing::account_id::{
259 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
260 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
261 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
262 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
263 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
264 };
265
266 #[test]
267 fn fungible_asset_from_id_and_value_words_fails_on_invalid_composition() -> anyhow::Result<()> {
268 let asset_id =
269 set_asset_metadata(FungibleAsset::mock(25).id(), AssetComposition::None.as_u8());
270
271 let err = FungibleAsset::from_id_and_value_words(
272 asset_id,
273 FungibleAsset::mock(5).to_value_word(),
274 )
275 .unwrap_err();
276 assert_matches!(err, AssetError::AssetCompositionMismatch {
277 faucet_id: _, expected, actual: _
278 } => {
279 assert_eq!(expected, AssetComposition::Fungible);
280 });
281
282 Ok(())
283 }
284
285 #[test]
286 fn fungible_asset_from_id_and_value_words_fails_on_invalid_asset_class() -> anyhow::Result<()> {
287 let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
288 let mut asset_id =
289 AssetId::new(AssetClass::default(), faucet_id, AssetComposition::Fungible)?.to_word();
290 asset_id[0] = Felt::from(1u32);
291 asset_id[1] = Felt::from(2u32);
292
293 let err = FungibleAsset::from_id_and_value_words(
294 asset_id,
295 FungibleAsset::mock(5).to_value_word(),
296 )
297 .unwrap_err();
298 assert_matches!(err, AssetError::FungibleAssetClassMustBeZero(_));
299
300 Ok(())
301 }
302
303 #[test]
304 fn fungible_asset_from_id_and_value_fails_on_invalid_value() -> anyhow::Result<()> {
305 let asset = FungibleAsset::mock(42);
306 let mut invalid_value = asset.to_value_word();
307 invalid_value[2] = Felt::from(5u32);
308
309 let err = FungibleAsset::from_id_and_value(asset.id(), invalid_value).unwrap_err();
310 assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
311
312 Ok(())
313 }
314
315 #[test]
316 fn test_fungible_asset_serde() -> anyhow::Result<()> {
317 for fungible_account_id in [
318 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
319 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
320 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
321 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
322 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
323 ] {
324 let account_id = AccountId::try_from(fungible_account_id).unwrap();
325 let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
326 assert_eq!(
327 fungible_asset,
328 FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
329 );
330 assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint());
331
332 assert_eq!(
333 fungible_asset,
334 FungibleAsset::from_id_and_value_words(
335 fungible_asset.to_id_word(),
336 fungible_asset.to_value_word()
337 )?
338 )
339 }
340
341 let non_fungible_asset = NonFungibleAsset::mock(&[4]);
342 let err = FungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap_err();
343 assert_matches!(err, DeserializationError::InvalidValue(msg) => {
344 assert!(msg.contains("expected fungible asset composition but found None"));
345 });
346
347 Ok(())
348 }
349
350 #[test]
351 fn test_asset_id_for_fungible_asset() {
352 let asset = FungibleAsset::mock(34);
353
354 assert_eq!(asset.id().faucet_id(), FungibleAsset::mock_issuer());
355 assert_eq!(asset.id().asset_class().prefix().as_canonical_u64(), 0);
356 assert_eq!(asset.id().asset_class().suffix().as_canonical_u64(), 0);
357 }
358}