miden_objects/asset/
fungible.rs

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