miden_protocol/asset/
fungible.rs1use alloc::string::ToString;
2use core::fmt;
3
4use super::vault::AssetVaultKey;
5use super::{AccountType, Asset, AssetError, Word};
6use crate::Felt;
7use crate::account::AccountId;
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 fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
53 if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
54 return Err(AssetError::FungibleFaucetIdTypeMismatch(faucet_id));
55 }
56
57 if amount > Self::MAX_AMOUNT {
58 return Err(AssetError::FungibleAssetAmountTooBig(amount));
59 }
60
61 Ok(Self { faucet_id, amount })
62 }
63
64 pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
75 if !key.asset_id().is_empty() {
76 return Err(AssetError::FungibleAssetIdMustBeZero(key.asset_id()));
77 }
78
79 if value[1] != Felt::ZERO || value[2] != Felt::ZERO || value[3] != Felt::ZERO {
80 return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value));
81 }
82
83 Self::new(key.faucet_id(), value[0].as_canonical_u64())
84 }
85
86 pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
96 let vault_key = AssetVaultKey::try_from(key)?;
97 Self::from_key_value(vault_key, value)
98 }
99
100 pub fn faucet_id(&self) -> AccountId {
105 self.faucet_id
106 }
107
108 pub fn amount(&self) -> u64 {
110 self.amount
111 }
112
113 pub fn is_from_same_faucet(&self, other: &Self) -> bool {
115 self.faucet_id == other.faucet_id
116 }
117
118 pub fn vault_key(&self) -> AssetVaultKey {
120 AssetVaultKey::new_fungible(self.faucet_id).expect("faucet ID should be of type fungible")
121 }
122
123 pub fn to_key_word(&self) -> Word {
125 self.vault_key().to_word()
126 }
127
128 pub fn to_value_word(&self) -> Word {
130 Word::new([
131 Felt::try_from(self.amount)
132 .expect("fungible asset should only allow amounts that fit into a felt"),
133 Felt::ZERO,
134 Felt::ZERO,
135 Felt::ZERO,
136 ])
137 }
138
139 #[allow(clippy::should_implement_trait)]
149 pub fn add(self, other: Self) -> Result<Self, AssetError> {
150 if self.faucet_id != other.faucet_id {
151 return Err(AssetError::FungibleAssetInconsistentFaucetIds {
152 original_issuer: self.faucet_id,
153 other_issuer: other.faucet_id,
154 });
155 }
156
157 let amount = self
158 .amount
159 .checked_add(other.amount)
160 .expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64");
161 if amount > Self::MAX_AMOUNT {
162 return Err(AssetError::FungibleAssetAmountTooBig(amount));
163 }
164
165 Ok(Self { faucet_id: self.faucet_id, amount })
166 }
167
168 #[allow(clippy::should_implement_trait)]
175 pub fn sub(self, other: Self) -> Result<Self, AssetError> {
176 if self.faucet_id != other.faucet_id {
177 return Err(AssetError::FungibleAssetInconsistentFaucetIds {
178 original_issuer: self.faucet_id,
179 other_issuer: other.faucet_id,
180 });
181 }
182
183 let amount = self.amount.checked_sub(other.amount).ok_or(
184 AssetError::FungibleAssetAmountNotSufficient {
185 minuend: self.amount,
186 subtrahend: other.amount,
187 },
188 )?;
189
190 Ok(FungibleAsset { faucet_id: self.faucet_id, amount })
191 }
192}
193
194impl From<FungibleAsset> for Asset {
195 fn from(asset: FungibleAsset) -> Self {
196 Asset::Fungible(asset)
197 }
198}
199
200impl fmt::Display for FungibleAsset {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(f, "{self:?}")
204 }
205}
206
207impl Serializable for FungibleAsset {
211 fn write_into<W: ByteWriter>(&self, target: &mut W) {
212 target.write(self.faucet_id);
215 target.write(self.amount);
216 }
217
218 fn get_size_hint(&self) -> usize {
219 self.faucet_id.get_size_hint() + self.amount.get_size_hint()
220 }
221}
222
223impl Deserializable for FungibleAsset {
224 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
225 let faucet_id: AccountId = source.read()?;
226 FungibleAsset::deserialize_with_faucet_id(faucet_id, source)
227 }
228}
229
230impl FungibleAsset {
231 pub(super) fn deserialize_with_faucet_id<R: ByteReader>(
234 faucet_id: AccountId,
235 source: &mut R,
236 ) -> Result<Self, DeserializationError> {
237 let amount: u64 = source.read()?;
238 FungibleAsset::new(faucet_id, amount)
239 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
240 }
241}
242
243#[cfg(test)]
247mod tests {
248 use assert_matches::assert_matches;
249
250 use super::*;
251 use crate::account::AccountId;
252 use crate::testing::account_id::{
253 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
254 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
255 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
256 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
257 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
258 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
259 };
260
261 #[test]
262 fn fungible_asset_from_key_value_words_fails_on_invalid_asset_id() -> anyhow::Result<()> {
263 let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
264 let invalid_key = Word::from([
265 Felt::from(1u32),
266 Felt::from(2u32),
267 faucet_id.suffix(),
268 faucet_id.prefix().as_felt(),
269 ]);
270
271 let err = FungibleAsset::from_key_value_words(
272 invalid_key,
273 FungibleAsset::mock(5).to_value_word(),
274 )
275 .unwrap_err();
276 assert_matches!(err, AssetError::FungibleAssetIdMustBeZero(_));
277
278 Ok(())
279 }
280
281 #[test]
282 fn fungible_asset_from_key_value_fails_on_invalid_value() -> anyhow::Result<()> {
283 let asset = FungibleAsset::mock(42);
284 let mut invalid_value = asset.to_value_word();
285 invalid_value[2] = Felt::from(5u32);
286
287 let err = FungibleAsset::from_key_value(asset.vault_key(), invalid_value).unwrap_err();
288 assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
289
290 Ok(())
291 }
292
293 #[test]
294 fn test_fungible_asset_serde() -> anyhow::Result<()> {
295 for fungible_account_id in [
296 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
297 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
298 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
299 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
300 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
301 ] {
302 let account_id = AccountId::try_from(fungible_account_id).unwrap();
303 let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
304 assert_eq!(
305 fungible_asset,
306 FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
307 );
308 assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint());
309
310 assert_eq!(
311 fungible_asset,
312 FungibleAsset::from_key_value_words(
313 fungible_asset.to_key_word(),
314 fungible_asset.to_value_word()
315 )?
316 )
317 }
318
319 let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap();
320 let asset = FungibleAsset::new(account_id, 50).unwrap();
321 let mut asset_bytes = asset.to_bytes();
322 assert_eq!(asset_bytes.len(), asset.get_size_hint());
323 assert_eq!(asset.get_size_hint(), FungibleAsset::SERIALIZED_SIZE);
324
325 let non_fungible_faucet_id =
326 AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
327
328 asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes());
330 let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
331 assert!(matches!(err, DeserializationError::InvalidValue(_)));
332
333 Ok(())
334 }
335
336 #[test]
337 fn test_vault_key_for_fungible_asset() {
338 let asset = FungibleAsset::mock(34);
339
340 assert_eq!(asset.vault_key().faucet_id(), FungibleAsset::mock_issuer());
341 assert_eq!(asset.vault_key().asset_id().prefix().as_canonical_u64(), 0);
342 assert_eq!(asset.vault_key().asset_id().suffix().as_canonical_u64(), 0);
343 }
344}