miden_objects/asset/
fungible.rs1use alloc::boxed::Box;
2use alloc::string::ToString;
3use core::fmt;
4
5use super::vault::AssetVaultKey;
6use super::{AccountType, Asset, AssetError, Felt, Word, ZERO, is_not_a_non_fungible_asset};
7use crate::account::{AccountId, AccountIdPrefix};
8use crate::utils::serde::{
9 ByteReader,
10 ByteWriter,
11 Deserializable,
12 DeserializationError,
13 Serializable,
14};
15
16#[derive(Debug, Copy, Clone, PartialEq, Eq)]
23pub struct FungibleAsset {
24 faucet_id: AccountId,
25 amount: u64,
26}
27
28impl FungibleAsset {
29 pub const MAX_AMOUNT: u64 = 2u64.pow(63) - 2u64.pow(31);
36
37 pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::<u64>();
41
42 pub const fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
51 let asset = Self { faucet_id, amount };
52 asset.validate()
53 }
54
55 pub(crate) fn new_unchecked(value: Word) -> FungibleAsset {
57 FungibleAsset {
58 faucet_id: AccountId::new_unchecked([value[3], value[2]]),
59 amount: value[0].as_int(),
60 }
61 }
62
63 pub fn faucet_id(&self) -> AccountId {
68 self.faucet_id
69 }
70
71 pub fn faucet_id_prefix(&self) -> AccountIdPrefix {
73 self.faucet_id.prefix()
74 }
75
76 pub fn amount(&self) -> u64 {
78 self.amount
79 }
80
81 pub fn is_from_same_faucet(&self, other: &Self) -> bool {
83 self.faucet_id == other.faucet_id
84 }
85
86 pub fn vault_key(&self) -> AssetVaultKey {
88 AssetVaultKey::from_account_id(self.faucet_id)
89 .expect("faucet ID should be of type fungible")
90 }
91
92 #[allow(clippy::should_implement_trait)]
102 pub fn add(self, other: Self) -> Result<Self, AssetError> {
103 if self.faucet_id != other.faucet_id {
104 return Err(AssetError::FungibleAssetInconsistentFaucetIds {
105 original_issuer: self.faucet_id,
106 other_issuer: other.faucet_id,
107 });
108 }
109
110 let amount = self
111 .amount
112 .checked_add(other.amount)
113 .expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64");
114 if amount > Self::MAX_AMOUNT {
115 return Err(AssetError::FungibleAssetAmountTooBig(amount));
116 }
117
118 Ok(Self { faucet_id: self.faucet_id, amount })
119 }
120
121 #[allow(clippy::should_implement_trait)]
128 pub fn sub(self, other: Self) -> Result<Self, AssetError> {
129 if self.faucet_id != other.faucet_id {
130 return Err(AssetError::FungibleAssetInconsistentFaucetIds {
131 original_issuer: self.faucet_id,
132 other_issuer: other.faucet_id,
133 });
134 }
135
136 let amount = self.amount.checked_sub(other.amount).ok_or(
137 AssetError::FungibleAssetAmountNotSufficient {
138 minuend: self.amount,
139 subtrahend: other.amount,
140 },
141 )?;
142
143 Ok(FungibleAsset { faucet_id: self.faucet_id, amount })
144 }
145
146 const fn validate(self) -> Result<Self, AssetError> {
155 let account_type = self.faucet_id.account_type();
156 if !matches!(account_type, AccountType::FungibleFaucet) {
157 return Err(AssetError::FungibleFaucetIdTypeMismatch(self.faucet_id));
158 }
159
160 if self.amount > Self::MAX_AMOUNT {
161 return Err(AssetError::FungibleAssetAmountTooBig(self.amount));
162 }
163
164 Ok(self)
165 }
166}
167
168impl From<FungibleAsset> for Word {
169 fn from(asset: FungibleAsset) -> Self {
170 let mut result = Word::empty();
171 result[0] = Felt::new(asset.amount);
172 result[2] = asset.faucet_id.suffix();
173 result[3] = asset.faucet_id.prefix().as_felt();
174 debug_assert!(is_not_a_non_fungible_asset(result));
175 result
176 }
177}
178
179impl From<FungibleAsset> for Asset {
180 fn from(asset: FungibleAsset) -> Self {
181 Asset::Fungible(asset)
182 }
183}
184
185impl TryFrom<Word> for FungibleAsset {
186 type Error = AssetError;
187
188 fn try_from(value: Word) -> Result<Self, Self::Error> {
189 if value[1] != ZERO {
190 return Err(AssetError::FungibleAssetExpectedZero(value));
191 }
192 let faucet_id = AccountId::try_from([value[3], value[2]])
193 .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
194 let amount = value[0].as_int();
195 Self::new(faucet_id, amount)
196 }
197}
198
199impl fmt::Display for FungibleAsset {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 write!(f, "{self:?}")
202 }
203}
204
205impl Serializable for FungibleAsset {
209 fn write_into<W: ByteWriter>(&self, target: &mut W) {
210 target.write(self.faucet_id);
213 target.write(self.amount);
214 }
215
216 fn get_size_hint(&self) -> usize {
217 self.faucet_id.get_size_hint() + self.amount.get_size_hint()
218 }
219}
220
221impl Deserializable for FungibleAsset {
222 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
223 let faucet_id_prefix: AccountIdPrefix = source.read()?;
224 FungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
225 }
226}
227
228impl FungibleAsset {
229 pub(super) fn deserialize_with_faucet_id_prefix<R: ByteReader>(
232 faucet_id_prefix: AccountIdPrefix,
233 source: &mut R,
234 ) -> Result<Self, DeserializationError> {
235 let suffix_bytes: [u8; 7] = source.read()?;
238 let prefix_bytes: [u8; 8] = faucet_id_prefix.into();
240 let mut id_bytes: [u8; 15] = [0; 15];
241 id_bytes[..8].copy_from_slice(&prefix_bytes);
242 id_bytes[8..].copy_from_slice(&suffix_bytes);
243
244 let faucet_id = AccountId::try_from(id_bytes)
245 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
246
247 let amount: u64 = source.read()?;
248 FungibleAsset::new(faucet_id, amount)
249 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
250 }
251}
252
253#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::account::AccountId;
260 use crate::testing::account_id::{
261 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
262 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
263 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
264 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
265 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
266 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
267 };
268
269 #[test]
270 fn test_fungible_asset_serde() {
271 for fungible_account_id in [
272 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
273 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
274 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
275 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
276 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
277 ] {
278 let account_id = AccountId::try_from(fungible_account_id).unwrap();
279 let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
280 assert_eq!(
281 fungible_asset,
282 FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
283 );
284 }
285
286 let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap();
287 let asset = FungibleAsset::new(account_id, 50).unwrap();
288 let mut asset_bytes = asset.to_bytes();
289 assert_eq!(asset_bytes.len(), asset.get_size_hint());
290 assert_eq!(asset.get_size_hint(), FungibleAsset::SERIALIZED_SIZE);
291
292 let non_fungible_faucet_id =
293 AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
294
295 asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes());
297 let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
298 assert!(matches!(err, DeserializationError::InvalidValue(_)));
299 }
300}