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 self.amount.to_word()
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::new(asset.id(), asset.to_value_word())
193 .expect("fungible asset should be a valid asset")
194 }
195}
196
197impl fmt::Display for FungibleAsset {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 write!(f, "{self:?}")
201 }
202}
203
204impl Serializable for FungibleAsset {
208 fn write_into<W: ByteWriter>(&self, target: &mut W) {
209 self.id().write_into(target);
211 target.write(self.amount.as_u64());
212 }
213
214 fn get_size_hint(&self) -> usize {
215 self.id().get_size_hint() + self.amount.as_u64().get_size_hint()
216 }
217}
218
219impl Deserializable for FungibleAsset {
220 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
221 let id = AssetId::read_from(source)?;
222
223 if !id.composition().is_fungible() {
224 return Err(DeserializationError::InvalidValue(format!(
225 "expected fungible asset composition but found {:?}",
226 id.composition()
227 )));
228 }
229 debug_assert!(
230 id.asset_class().is_empty(),
231 "asset ID should validate asset class is empty for composition fungible"
232 );
233
234 let amount: u64 = source.read()?;
235
236 FungibleAsset::new(id.faucet_id(), amount)
237 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
238 }
239}
240
241#[cfg(test)]
245mod tests {
246 use assert_matches::assert_matches;
247
248 use super::*;
249 use crate::account::AccountId;
250 use crate::asset::NonFungibleAsset;
251 use crate::asset::tests::set_asset_metadata;
252 use crate::testing::account_id::{
253 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
254 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
255 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
256 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
257 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
258 };
259
260 #[test]
261 fn fungible_asset_from_id_and_value_words_fails_on_invalid_composition() -> anyhow::Result<()> {
262 let asset_id = set_asset_metadata(
263 FungibleAsset::mock(25).id(),
264 AssetId::encode_metadata(AssetComposition::None),
265 );
266
267 let err = FungibleAsset::from_id_and_value_words(
268 asset_id,
269 FungibleAsset::mock(5).to_value_word(),
270 )
271 .unwrap_err();
272 assert_matches!(err, AssetError::AssetCompositionMismatch {
273 faucet_id: _, expected, actual: _
274 } => {
275 assert_eq!(expected, AssetComposition::Fungible);
276 });
277
278 Ok(())
279 }
280
281 #[test]
282 fn fungible_asset_from_id_and_value_words_fails_on_invalid_asset_class() -> anyhow::Result<()> {
283 let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
284 let mut asset_id =
285 AssetId::new(AssetClass::default(), faucet_id, AssetComposition::Fungible)?.to_word();
286 asset_id[0] = Felt::from(1u32);
287 asset_id[1] = Felt::from(2u32);
288
289 let err = FungibleAsset::from_id_and_value_words(
290 asset_id,
291 FungibleAsset::mock(5).to_value_word(),
292 )
293 .unwrap_err();
294 assert_matches!(err, AssetError::FungibleAssetClassMustBeZero(_));
295
296 Ok(())
297 }
298
299 #[test]
300 fn fungible_asset_from_id_and_value_fails_on_invalid_value() -> anyhow::Result<()> {
301 let asset = FungibleAsset::mock(42);
302 let mut invalid_value = asset.to_value_word();
303 invalid_value[2] = Felt::from(5u32);
304
305 let err = FungibleAsset::from_id_and_value(asset.id(), invalid_value).unwrap_err();
306 assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
307
308 Ok(())
309 }
310
311 #[test]
312 fn test_fungible_asset_serde() -> anyhow::Result<()> {
313 for fungible_account_id in [
314 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
315 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
316 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
317 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
318 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
319 ] {
320 let account_id = AccountId::try_from(fungible_account_id).unwrap();
321 let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
322 assert_eq!(
323 fungible_asset,
324 FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
325 );
326 assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint());
327
328 assert_eq!(
329 fungible_asset,
330 FungibleAsset::from_id_and_value_words(
331 fungible_asset.to_id_word(),
332 fungible_asset.to_value_word()
333 )?
334 )
335 }
336
337 let non_fungible_asset = NonFungibleAsset::mock(&[4]);
338 let err = FungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap_err();
339 assert_matches!(err, DeserializationError::InvalidValue(msg) => {
340 assert!(msg.contains("expected fungible asset composition but found None"));
341 });
342
343 Ok(())
344 }
345
346 #[test]
347 fn test_asset_id_for_fungible_asset() {
348 let asset = FungibleAsset::mock(34);
349
350 assert_eq!(asset.id().faucet_id(), FungibleAsset::mock_issuer());
351 assert_eq!(asset.id().asset_class().prefix().as_canonical_u64(), 0);
352 assert_eq!(asset.id().asset_class().suffix().as_canonical_u64(), 0);
353 }
354}