miden_objects/asset/
fungible.rs

1use 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// FUNGIBLE ASSET
17// ================================================================================================
18/// A fungible asset.
19///
20/// A fungible asset consists of a faucet ID of the faucet which issued the asset as well as the
21/// asset amount. Asset amount is guaranteed to be 2^63 - 1 or smaller.
22#[derive(Debug, Copy, Clone, PartialEq, Eq)]
23pub struct FungibleAsset {
24    faucet_id: AccountId,
25    amount: u64,
26}
27
28impl FungibleAsset {
29    // CONSTANTS
30    // --------------------------------------------------------------------------------------------
31    /// Specifies the maximum amount a fungible asset can represent.
32    ///
33    /// This number was chosen so that it can be represented as a positive and negative number in a
34    /// field element. See `account_delta.masm` for more details on how this number was chosen.
35    pub const MAX_AMOUNT: u64 = 2u64.pow(63) - 2u64.pow(31);
36
37    /// The serialized size of a [`FungibleAsset`] in bytes.
38    ///
39    /// Currently an account ID (15 bytes) plus an amount (u64).
40    pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::<u64>();
41
42    // CONSTRUCTOR
43    // --------------------------------------------------------------------------------------------
44    /// Returns a fungible asset instantiated with the provided faucet ID and amount.
45    ///
46    /// # Errors
47    /// Returns an error if:
48    /// - The faucet_id is not a valid fungible faucet ID.
49    /// - The provided amount is greater than 2^63 - 1.
50    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    /// Creates a new [FungibleAsset] without checking its validity.
56    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    // PUBLIC ACCESSORS
64    // --------------------------------------------------------------------------------------------
65
66    /// Return ID of the faucet which issued this asset.
67    pub fn faucet_id(&self) -> AccountId {
68        self.faucet_id
69    }
70
71    /// Return ID prefix of the faucet which issued this asset.
72    pub fn faucet_id_prefix(&self) -> AccountIdPrefix {
73        self.faucet_id.prefix()
74    }
75
76    /// Returns the amount of this asset.
77    pub fn amount(&self) -> u64 {
78        self.amount
79    }
80
81    /// Returns true if this and the other assets were issued from the same faucet.
82    pub fn is_from_same_faucet(&self, other: &Self) -> bool {
83        self.faucet_id == other.faucet_id
84    }
85
86    /// Returns the key which is used to store this asset in the account vault.
87    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    // OPERATIONS
93    // --------------------------------------------------------------------------------------------
94
95    /// Adds two fungible assets together and returns the result.
96    ///
97    /// # Errors
98    /// Returns an error if:
99    /// - The assets were not issued by the same faucet.
100    /// - The total value of assets is greater than or equal to 2^63.
101    #[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    /// Subtracts a fungible asset from another and returns the result.
122    ///
123    /// # Errors
124    /// Returns an error if:
125    /// - The assets were not issued by the same faucet.
126    /// - The final amount would be negative.
127    #[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    // HELPER FUNCTIONS
147    // --------------------------------------------------------------------------------------------
148
149    /// Validates this fungible asset.
150    /// # Errors
151    /// Returns an error if:
152    /// - The faucet_id is not a valid fungible faucet ID.
153    /// - The provided amount is greater than 2^63 - 1.
154    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
205// SERIALIZATION
206// ================================================================================================
207
208impl Serializable for FungibleAsset {
209    fn write_into<W: ByteWriter>(&self, target: &mut W) {
210        // All assets should serialize their faucet ID at the first position to allow them to be
211        // distinguishable during deserialization.
212        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    /// Deserializes a [`FungibleAsset`] from an [`AccountIdPrefix`] and the remaining data from the
230    /// given `source`.
231    pub(super) fn deserialize_with_faucet_id_prefix<R: ByteReader>(
232        faucet_id_prefix: AccountIdPrefix,
233        source: &mut R,
234    ) -> Result<Self, DeserializationError> {
235        // The 8 bytes of the prefix have already been read, so we only need to read the remaining 7
236        // bytes of the account ID's 15 total bytes.
237        let suffix_bytes: [u8; 7] = source.read()?;
238        // Convert prefix back to bytes so we can call the TryFrom<[u8; 15]> impl.
239        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// TESTS
254// ================================================================================================
255
256#[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        // Set invalid Faucet ID.
296        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}