Skip to main content

miden_protocol/asset/
fungible.rs

1use alloc::string::ToString;
2use core::fmt;
3
4use super::vault::AssetVaultKey;
5use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word};
6use crate::Felt;
7use crate::account::AccountId;
8use crate::asset::AssetId;
9use crate::utils::serde::{
10    ByteReader,
11    ByteWriter,
12    Deserializable,
13    DeserializationError,
14    Serializable,
15};
16
17// FUNGIBLE ASSET
18// ================================================================================================
19/// A fungible asset.
20///
21/// A fungible asset consists of a faucet ID of the faucet which issued the asset as well as the
22/// asset amount. Asset amount is guaranteed to be 2^63 - 1 or smaller.
23///
24/// The fungible asset can have callbacks to the faucet enabled or disabled, depending on
25/// [`AssetCallbackFlag`]. See [`AssetCallbacks`](crate::asset::AssetCallbacks) for more details.
26#[derive(Debug, Copy, Clone, PartialEq, Eq)]
27pub struct FungibleAsset {
28    faucet_id: AccountId,
29    amount: u64,
30    callbacks: AssetCallbackFlag,
31}
32
33impl FungibleAsset {
34    // CONSTANTS
35    // --------------------------------------------------------------------------------------------
36    /// Specifies the maximum amount a fungible asset can represent.
37    ///
38    /// This number was chosen so that it can be represented as a positive and negative number in a
39    /// field element. See `account_delta.masm` for more details on how this number was chosen.
40    pub const MAX_AMOUNT: u64 = 2u64.pow(63) - 2u64.pow(31);
41
42    /// The serialized size of a [`FungibleAsset`] in bytes.
43    ///
44    /// An account ID (15 bytes) plus an amount (u64) plus a callbacks flag (u8).
45    pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE
46        + core::mem::size_of::<u64>()
47        + AssetCallbackFlag::SERIALIZED_SIZE;
48
49    // CONSTRUCTOR
50    // --------------------------------------------------------------------------------------------
51
52    /// Returns a fungible asset instantiated with the provided faucet ID and amount.
53    ///
54    /// # Errors
55    ///
56    /// Returns an error if:
57    /// - The faucet ID is not a valid fungible faucet ID.
58    /// - The provided amount is greater than [`FungibleAsset::MAX_AMOUNT`].
59    pub fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
60        if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
61            return Err(AssetError::FungibleFaucetIdTypeMismatch(faucet_id));
62        }
63
64        if amount > Self::MAX_AMOUNT {
65            return Err(AssetError::FungibleAssetAmountTooBig(amount));
66        }
67
68        Ok(Self {
69            faucet_id,
70            amount,
71            callbacks: AssetCallbackFlag::default(),
72        })
73    }
74
75    /// Creates a fungible asset from the provided key and value.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if:
80    /// - The provided key does not contain a valid faucet ID.
81    /// - The provided key's asset ID limbs are not zero.
82    /// - The faucet ID is not a fungible faucet ID.
83    /// - The provided value's amount is greater than [`FungibleAsset::MAX_AMOUNT`] or its three
84    ///   most significant elements are not zero.
85    pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
86        if !key.asset_id().is_empty() {
87            return Err(AssetError::FungibleAssetIdMustBeZero(key.asset_id()));
88        }
89
90        if value[1] != Felt::ZERO || value[2] != Felt::ZERO || value[3] != Felt::ZERO {
91            return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value));
92        }
93
94        let mut asset = Self::new(key.faucet_id(), value[0].as_canonical_u64())?;
95        asset.callbacks = key.callback_flag();
96
97        Ok(asset)
98    }
99
100    /// Creates a fungible asset from the provided key and value.
101    ///
102    /// Prefer [`Self::from_key_value`] for more type safety.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if:
107    /// - The provided key does not contain a valid faucet ID.
108    /// - [`Self::from_key_value`] fails.
109    pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
110        let vault_key = AssetVaultKey::try_from(key)?;
111        Self::from_key_value(vault_key, value)
112    }
113
114    /// Returns a copy of this asset with the given [`AssetCallbackFlag`].
115    pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self {
116        self.callbacks = callbacks;
117        self
118    }
119
120    // PUBLIC ACCESSORS
121    // --------------------------------------------------------------------------------------------
122
123    /// Return ID of the faucet which issued this asset.
124    pub fn faucet_id(&self) -> AccountId {
125        self.faucet_id
126    }
127
128    /// Returns the amount of this asset.
129    pub fn amount(&self) -> u64 {
130        self.amount
131    }
132
133    /// Returns true if this and the other asset were issued from the same faucet.
134    pub fn is_same(&self, other: &Self) -> bool {
135        self.vault_key() == other.vault_key()
136    }
137
138    /// Returns the [`AssetCallbackFlag`] of this asset.
139    pub fn callbacks(&self) -> AssetCallbackFlag {
140        self.callbacks
141    }
142
143    /// Returns the key which is used to store this asset in the account vault.
144    pub fn vault_key(&self) -> AssetVaultKey {
145        AssetVaultKey::new(AssetId::default(), self.faucet_id, self.callbacks)
146            .expect("faucet ID should be of type fungible")
147    }
148
149    /// Returns the asset's key encoded to a [`Word`].
150    pub fn to_key_word(&self) -> Word {
151        self.vault_key().to_word()
152    }
153
154    /// Returns the asset's value encoded to a [`Word`].
155    pub fn to_value_word(&self) -> Word {
156        Word::new([
157            Felt::try_from(self.amount)
158                .expect("fungible asset should only allow amounts that fit into a felt"),
159            Felt::ZERO,
160            Felt::ZERO,
161            Felt::ZERO,
162        ])
163    }
164
165    // OPERATIONS
166    // --------------------------------------------------------------------------------------------
167
168    /// Adds two fungible assets together and returns the result.
169    ///
170    /// # Errors
171    /// Returns an error if:
172    /// - The assets do not have the same vault key (i.e. different faucet or callback flags).
173    /// - The total value of assets is greater than or equal to 2^63.
174    #[allow(clippy::should_implement_trait)]
175    pub fn add(self, other: Self) -> Result<Self, AssetError> {
176        if !self.is_same(&other) {
177            return Err(AssetError::FungibleAssetInconsistentVaultKeys {
178                original_key: self.vault_key(),
179                other_key: other.vault_key(),
180            });
181        }
182
183        let amount = self
184            .amount
185            .checked_add(other.amount)
186            .expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64");
187        if amount > Self::MAX_AMOUNT {
188            return Err(AssetError::FungibleAssetAmountTooBig(amount));
189        }
190
191        Ok(Self {
192            faucet_id: self.faucet_id,
193            amount,
194            callbacks: self.callbacks,
195        })
196    }
197
198    /// Subtracts a fungible asset from another and returns the result.
199    ///
200    /// # Errors
201    /// Returns an error if:
202    /// - The assets do not have the same vault key (i.e. different faucet or callback flags).
203    /// - The final amount would be negative.
204    #[allow(clippy::should_implement_trait)]
205    pub fn sub(self, other: Self) -> Result<Self, AssetError> {
206        if !self.is_same(&other) {
207            return Err(AssetError::FungibleAssetInconsistentVaultKeys {
208                original_key: self.vault_key(),
209                other_key: other.vault_key(),
210            });
211        }
212
213        let amount = self.amount.checked_sub(other.amount).ok_or(
214            AssetError::FungibleAssetAmountNotSufficient {
215                minuend: self.amount,
216                subtrahend: other.amount,
217            },
218        )?;
219
220        Ok(FungibleAsset {
221            faucet_id: self.faucet_id,
222            amount,
223            callbacks: self.callbacks,
224        })
225    }
226}
227
228impl From<FungibleAsset> for Asset {
229    fn from(asset: FungibleAsset) -> Self {
230        Asset::Fungible(asset)
231    }
232}
233
234impl fmt::Display for FungibleAsset {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        // TODO: Replace with hex representation?
237        write!(f, "{self:?}")
238    }
239}
240
241// SERIALIZATION
242// ================================================================================================
243
244impl Serializable for FungibleAsset {
245    fn write_into<W: ByteWriter>(&self, target: &mut W) {
246        // All assets should serialize their faucet ID at the first position to allow them to be
247        // distinguishable during deserialization.
248        target.write(self.faucet_id);
249        target.write(self.amount);
250        target.write(self.callbacks);
251    }
252
253    fn get_size_hint(&self) -> usize {
254        self.faucet_id.get_size_hint()
255            + self.amount.get_size_hint()
256            + self.callbacks.get_size_hint()
257    }
258}
259
260impl Deserializable for FungibleAsset {
261    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
262        let faucet_id: AccountId = source.read()?;
263        FungibleAsset::deserialize_with_faucet_id(faucet_id, source)
264    }
265}
266
267impl FungibleAsset {
268    /// Deserializes a [`FungibleAsset`] from an [`AccountId`] and the remaining data from the given
269    /// `source`.
270    pub(super) fn deserialize_with_faucet_id<R: ByteReader>(
271        faucet_id: AccountId,
272        source: &mut R,
273    ) -> Result<Self, DeserializationError> {
274        let amount: u64 = source.read()?;
275        let callbacks = source.read()?;
276
277        let asset = FungibleAsset::new(faucet_id, amount)
278            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?
279            .with_callbacks(callbacks);
280
281        Ok(asset)
282    }
283}
284
285// TESTS
286// ================================================================================================
287
288#[cfg(test)]
289mod tests {
290    use assert_matches::assert_matches;
291
292    use super::*;
293    use crate::account::AccountId;
294    use crate::testing::account_id::{
295        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
296        ACCOUNT_ID_PRIVATE_NON_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
303    #[test]
304    fn fungible_asset_from_key_value_words_fails_on_invalid_asset_id() -> anyhow::Result<()> {
305        let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
306        let invalid_key = Word::from([
307            Felt::from(1u32),
308            Felt::from(2u32),
309            faucet_id.suffix(),
310            faucet_id.prefix().as_felt(),
311        ]);
312
313        let err = FungibleAsset::from_key_value_words(
314            invalid_key,
315            FungibleAsset::mock(5).to_value_word(),
316        )
317        .unwrap_err();
318        assert_matches!(err, AssetError::FungibleAssetIdMustBeZero(_));
319
320        Ok(())
321    }
322
323    #[test]
324    fn fungible_asset_from_key_value_fails_on_invalid_value() -> anyhow::Result<()> {
325        let asset = FungibleAsset::mock(42);
326        let mut invalid_value = asset.to_value_word();
327        invalid_value[2] = Felt::from(5u32);
328
329        let err = FungibleAsset::from_key_value(asset.vault_key(), invalid_value).unwrap_err();
330        assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
331
332        Ok(())
333    }
334
335    #[test]
336    fn test_fungible_asset_serde() -> anyhow::Result<()> {
337        for fungible_account_id in [
338            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
339            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
340            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
341            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
342            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
343        ] {
344            let account_id = AccountId::try_from(fungible_account_id).unwrap();
345            let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
346            assert_eq!(
347                fungible_asset,
348                FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
349            );
350            assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint());
351
352            assert_eq!(
353                fungible_asset,
354                FungibleAsset::from_key_value_words(
355                    fungible_asset.to_key_word(),
356                    fungible_asset.to_value_word()
357                )?
358            )
359        }
360
361        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap();
362        let asset = FungibleAsset::new(account_id, 50).unwrap();
363        let mut asset_bytes = asset.to_bytes();
364        assert_eq!(asset_bytes.len(), asset.get_size_hint());
365        assert_eq!(asset.get_size_hint(), FungibleAsset::SERIALIZED_SIZE);
366
367        let non_fungible_faucet_id =
368            AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
369
370        // Set invalid Faucet ID.
371        asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes());
372        let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
373        assert!(matches!(err, DeserializationError::InvalidValue(_)));
374
375        Ok(())
376    }
377
378    #[test]
379    fn test_vault_key_for_fungible_asset() {
380        let asset = FungibleAsset::mock(34);
381
382        assert_eq!(asset.vault_key().faucet_id(), FungibleAsset::mock_issuer());
383        assert_eq!(asset.vault_key().asset_id().prefix().as_canonical_u64(), 0);
384        assert_eq!(asset.vault_key().asset_id().suffix().as_canonical_u64(), 0);
385    }
386}