miden_objects/asset/
fungible.rs1use alloc::{boxed::Box, string::ToString};
2use core::fmt;
3
4use vm_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
5use vm_processor::DeserializationError;
6
7use super::{is_not_a_non_fungible_asset, AccountType, Asset, AssetError, Felt, Word, ZERO};
8use crate::account::{AccountId, AccountIdPrefix};
9
10#[derive(Debug, Copy, Clone, PartialEq, Eq)]
17pub struct FungibleAsset {
18 faucet_id: AccountId,
19 amount: u64,
20}
21
22impl FungibleAsset {
23 pub const MAX_AMOUNT: u64 = (1_u64 << 63) - 1;
27
28 pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::<u64>();
32
33 pub const fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
42 let asset = Self { faucet_id, amount };
43 asset.validate()
44 }
45
46 pub(crate) fn new_unchecked(value: Word) -> FungibleAsset {
48 FungibleAsset {
49 faucet_id: AccountId::new_unchecked([value[3], value[2]]),
50 amount: value[0].as_int(),
51 }
52 }
53
54 pub fn faucet_id(&self) -> AccountId {
59 self.faucet_id
60 }
61
62 pub fn faucet_id_prefix(&self) -> AccountIdPrefix {
64 self.faucet_id.prefix()
65 }
66
67 pub fn amount(&self) -> u64 {
69 self.amount
70 }
71
72 pub fn is_from_same_faucet(&self, other: &Self) -> bool {
74 self.faucet_id == other.faucet_id
75 }
76
77 pub fn vault_key(&self) -> Word {
79 Self::vault_key_from_faucet(self.faucet_id)
80 }
81
82 #[allow(clippy::should_implement_trait)]
92 pub fn add(self, other: Self) -> Result<Self, AssetError> {
93 if self.faucet_id != other.faucet_id {
94 return Err(AssetError::FungibleAssetInconsistentFaucetIds {
95 original_issuer: self.faucet_id,
96 other_issuer: other.faucet_id,
97 });
98 }
99
100 let amount = self
101 .amount
102 .checked_add(other.amount)
103 .expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64");
104 if amount > Self::MAX_AMOUNT {
105 return Err(AssetError::FungibleAssetAmountTooBig(amount));
106 }
107
108 Ok(Self { faucet_id: self.faucet_id, amount })
109 }
110
111 pub fn sub(&mut self, amount: u64) -> Result<Self, AssetError> {
116 self.amount = self.amount.checked_sub(amount).ok_or(
117 AssetError::FungibleAssetAmountNotSufficient {
118 minuend: self.amount,
119 subtrahend: amount,
120 },
121 )?;
122
123 Ok(FungibleAsset { faucet_id: self.faucet_id, amount })
124 }
125
126 const fn validate(self) -> Result<Self, AssetError> {
135 let account_type = self.faucet_id.account_type();
136 if !matches!(account_type, AccountType::FungibleFaucet) {
137 return Err(AssetError::FungibleFaucetIdTypeMismatch(self.faucet_id));
138 }
139
140 if self.amount > Self::MAX_AMOUNT {
141 return Err(AssetError::FungibleAssetAmountTooBig(self.amount));
142 }
143
144 Ok(self)
145 }
146
147 pub(super) fn vault_key_from_faucet(faucet_id: AccountId) -> Word {
149 let mut key = Word::default();
150 key[2] = faucet_id.suffix();
151 key[3] = faucet_id.prefix().as_felt();
152 key
153 }
154}
155
156impl From<FungibleAsset> for Word {
157 fn from(asset: FungibleAsset) -> Self {
158 let mut result = Word::default();
159 result[0] = Felt::new(asset.amount);
160 result[2] = asset.faucet_id.suffix();
161 result[3] = asset.faucet_id.prefix().as_felt();
162 debug_assert!(is_not_a_non_fungible_asset(result));
163 result
164 }
165}
166
167impl From<FungibleAsset> for Asset {
168 fn from(asset: FungibleAsset) -> Self {
169 Asset::Fungible(asset)
170 }
171}
172
173impl TryFrom<Word> for FungibleAsset {
174 type Error = AssetError;
175
176 fn try_from(value: Word) -> Result<Self, Self::Error> {
177 if value[1] != ZERO {
178 return Err(AssetError::FungibleAssetExpectedZero(value));
179 }
180 let faucet_id = AccountId::try_from([value[3], value[2]])
181 .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
182 let amount = value[0].as_int();
183 Self::new(faucet_id, amount)
184 }
185}
186
187impl fmt::Display for FungibleAsset {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 write!(f, "{:?}", self)
190 }
191}
192
193impl Serializable for FungibleAsset {
197 fn write_into<W: ByteWriter>(&self, target: &mut W) {
198 target.write(self.faucet_id);
201 target.write(self.amount);
202 }
203
204 fn get_size_hint(&self) -> usize {
205 self.faucet_id.get_size_hint() + self.amount.get_size_hint()
206 }
207}
208
209impl Deserializable for FungibleAsset {
210 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
211 let faucet_id_prefix: AccountIdPrefix = source.read()?;
212 FungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
213 }
214}
215
216impl FungibleAsset {
217 pub(super) fn deserialize_with_faucet_id_prefix<R: ByteReader>(
220 faucet_id_prefix: AccountIdPrefix,
221 source: &mut R,
222 ) -> Result<Self, DeserializationError> {
223 let suffix_bytes: [u8; 7] = source.read()?;
226 let prefix_bytes: [u8; 8] = faucet_id_prefix.into();
228 let mut id_bytes: [u8; 15] = [0; 15];
229 id_bytes[..8].copy_from_slice(&prefix_bytes);
230 id_bytes[8..].copy_from_slice(&suffix_bytes);
231
232 let faucet_id = AccountId::try_from(id_bytes)
233 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
234
235 let amount: u64 = source.read()?;
236 FungibleAsset::new(faucet_id, amount)
237 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
238 }
239}
240
241#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::{
248 account::AccountId,
249 testing::account_id::{
250 ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
251 ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2,
252 ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN,
253 },
254 };
255
256 #[test]
257 fn test_fungible_asset_serde() {
258 for fungible_account_id in [
259 ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN,
260 ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
261 ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1,
262 ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2,
263 ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3,
264 ] {
265 let account_id = AccountId::try_from(fungible_account_id).unwrap();
266 let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
267 assert_eq!(
268 fungible_asset,
269 FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
270 );
271 }
272
273 let account_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3).unwrap();
274 let asset = FungibleAsset::new(account_id, 50).unwrap();
275 let mut asset_bytes = asset.to_bytes();
276 assert_eq!(asset_bytes.len(), asset.get_size_hint());
277 assert_eq!(asset.get_size_hint(), FungibleAsset::SERIALIZED_SIZE);
278
279 let non_fungible_faucet_id =
280 AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap();
281
282 asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes());
284 let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
285 assert!(matches!(err, DeserializationError::InvalidValue(_)));
286 }
287}