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