Skip to main content

miden_protocol/asset/
fungible.rs

1use alloc::string::ToString;
2use core::fmt;
3
4use super::vault::AssetVaultKey;
5use super::{Asset, AssetAmount, AssetCallbackFlag, AssetComposition, 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: AssetAmount,
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: AssetAmount = AssetAmount::MAX;
41
42    /// The serialized size of a [`FungibleAsset`] in bytes.
43    ///
44    /// A composition byte (u8) plus an account ID (15 bytes) plus an amount (u64) plus a
45    /// callbacks flag (u8).
46    pub const SERIALIZED_SIZE: usize = AssetComposition::SERIALIZED_SIZE
47        + AccountId::SERIALIZED_SIZE
48        + core::mem::size_of::<u64>()
49        + AssetCallbackFlag::SERIALIZED_SIZE;
50
51    // CONSTRUCTOR
52    // --------------------------------------------------------------------------------------------
53
54    /// Returns a fungible asset instantiated with the provided faucet ID and amount.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if:
59    /// - The provided amount is greater than [`FungibleAsset::MAX_AMOUNT`].
60    pub fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
61        // TODO: Take AssetAmount as input, then make the function infallible.
62        let amount = AssetAmount::new(amount)?;
63
64        Ok(Self {
65            faucet_id,
66            amount,
67            callbacks: AssetCallbackFlag::default(),
68        })
69    }
70
71    /// Creates a fungible asset from the provided key and value.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if:
76    /// - The provided key does not contain a valid faucet ID.
77    /// - The provided key's does not have [`AssetComposition::Fungible`] set.
78    /// - The provided key's asset ID limbs are not zero.
79    /// - The provided value's amount is greater than [`FungibleAsset::MAX_AMOUNT`] or its three
80    ///   most significant elements are not zero.
81    pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
82        if !key.composition().is_fungible() {
83            return Err(AssetError::AssetCompositionMismatch {
84                faucet_id: key.faucet_id(),
85                expected: AssetComposition::Fungible,
86                actual: key.composition(),
87            });
88        }
89
90        if !key.asset_id().is_empty() {
91            return Err(AssetError::FungibleAssetIdMustBeZero(key.asset_id()));
92        }
93
94        if value[1] != Felt::ZERO || value[2] != Felt::ZERO || value[3] != Felt::ZERO {
95            return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value));
96        }
97
98        let mut asset = Self::new(key.faucet_id(), value[0].as_canonical_u64())?;
99        asset.callbacks = key.callback_flag();
100
101        Ok(asset)
102    }
103
104    /// Creates a fungible asset from the provided key and value.
105    ///
106    /// Prefer [`Self::from_key_value`] for more type safety.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if:
111    /// - [`Self::from_key_value`] fails.
112    pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
113        let vault_key = AssetVaultKey::try_from(key)?;
114        Self::from_key_value(vault_key, value)
115    }
116
117    /// Returns a copy of this asset with the given [`AssetCallbackFlag`].
118    pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self {
119        self.callbacks = callbacks;
120        self
121    }
122
123    // PUBLIC ACCESSORS
124    // --------------------------------------------------------------------------------------------
125
126    /// Return ID of the faucet which issued this asset.
127    pub fn faucet_id(&self) -> AccountId {
128        self.faucet_id
129    }
130
131    /// Returns the amount of this asset.
132    pub fn amount(&self) -> AssetAmount {
133        self.amount
134    }
135
136    /// Returns true if this and the other asset were issued from the same faucet.
137    pub fn is_same(&self, other: &Self) -> bool {
138        self.vault_key() == other.vault_key()
139    }
140
141    /// Returns the [`AssetCallbackFlag`] of this asset.
142    pub fn callbacks(&self) -> AssetCallbackFlag {
143        self.callbacks
144    }
145
146    /// Returns the key which is used to store this asset in the account vault.
147    pub fn vault_key(&self) -> AssetVaultKey {
148        AssetVaultKey::new(
149            AssetId::default(),
150            self.faucet_id,
151            AssetComposition::Fungible,
152            self.callbacks,
153        )
154        .expect("default asset id should be valid for fungible composition")
155    }
156
157    /// Returns the asset's key encoded to a [`Word`].
158    pub fn to_key_word(&self) -> Word {
159        self.vault_key().to_word()
160    }
161
162    /// Returns the asset's value encoded to a [`Word`].
163    pub fn to_value_word(&self) -> Word {
164        Word::new([Felt::from(self.amount), Felt::ZERO, Felt::ZERO, Felt::ZERO])
165    }
166
167    // OPERATIONS
168    // --------------------------------------------------------------------------------------------
169
170    /// Adds two fungible assets together and returns the result.
171    ///
172    /// # Errors
173    /// Returns an error if:
174    /// - The assets do not have the same vault key (i.e. different faucet or callback flags).
175    /// - The total value of assets is greater than or equal to 2^63.
176    #[allow(clippy::should_implement_trait)]
177    pub fn add(self, other: Self) -> Result<Self, AssetError> {
178        if !self.is_same(&other) {
179            return Err(AssetError::FungibleAssetInconsistentVaultKeys {
180                original_key: self.vault_key(),
181                other_key: other.vault_key(),
182            });
183        }
184
185        let amount = (self.amount + other.amount)?;
186
187        Ok(Self {
188            faucet_id: self.faucet_id,
189            amount,
190            callbacks: self.callbacks,
191        })
192    }
193
194    /// Subtracts a fungible asset from another and returns the result.
195    ///
196    /// # Errors
197    /// Returns an error if:
198    /// - The assets do not have the same vault key (i.e. different faucet or callback flags).
199    /// - The final amount would be negative.
200    #[allow(clippy::should_implement_trait)]
201    pub fn sub(self, other: Self) -> Result<Self, AssetError> {
202        if !self.is_same(&other) {
203            return Err(AssetError::FungibleAssetInconsistentVaultKeys {
204                original_key: self.vault_key(),
205                other_key: other.vault_key(),
206            });
207        }
208
209        let amount = (self.amount - other.amount)?;
210
211        Ok(FungibleAsset {
212            faucet_id: self.faucet_id,
213            amount,
214            callbacks: self.callbacks,
215        })
216    }
217}
218
219impl From<FungibleAsset> for Asset {
220    fn from(asset: FungibleAsset) -> Self {
221        Asset::Fungible(asset)
222    }
223}
224
225impl fmt::Display for FungibleAsset {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        // TODO: Replace with hex representation?
228        write!(f, "{self:?}")
229    }
230}
231
232// SERIALIZATION
233// ================================================================================================
234
235impl Serializable for FungibleAsset {
236    fn write_into<W: ByteWriter>(&self, target: &mut W) {
237        // Lead with the asset composition byte to distinguish asset types on the wire.
238        target.write(AssetComposition::Fungible);
239        target.write(self.faucet_id);
240        target.write(self.amount.as_u64());
241        target.write(self.callbacks);
242    }
243
244    fn get_size_hint(&self) -> usize {
245        AssetComposition::SERIALIZED_SIZE
246            + self.faucet_id.get_size_hint()
247            + self.amount.as_u64().get_size_hint()
248            + self.callbacks.get_size_hint()
249    }
250}
251
252impl Deserializable for FungibleAsset {
253    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
254        let composition: AssetComposition = source.read()?;
255        if !composition.is_fungible() {
256            return Err(DeserializationError::InvalidValue(format!(
257                "expected fungible asset composition but found {composition:?}"
258            )));
259        }
260        FungibleAsset::deserialize_body(source)
261    }
262}
263
264impl FungibleAsset {
265    /// Reads the remaining body of a fungible asset, after the leading composition byte has
266    /// already been consumed.
267    pub(super) fn deserialize_body<R: ByteReader>(
268        source: &mut R,
269    ) -> Result<Self, DeserializationError> {
270        let faucet_id: AccountId = source.read()?;
271        let amount: u64 = source.read()?;
272        let callbacks = source.read()?;
273
274        let asset = FungibleAsset::new(faucet_id, amount)
275            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?
276            .with_callbacks(callbacks);
277
278        Ok(asset)
279    }
280}
281
282// TESTS
283// ================================================================================================
284
285#[cfg(test)]
286mod tests {
287    use assert_matches::assert_matches;
288
289    use super::*;
290    use crate::account::AccountId;
291    use crate::asset::NonFungibleAsset;
292    use crate::asset::tests::set_asset_metadata;
293    use crate::testing::account_id::{
294        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
295        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
296        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
297        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
298        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
299    };
300
301    #[test]
302    fn fungible_asset_from_key_value_words_fails_on_invalid_composition() -> anyhow::Result<()> {
303        let asset_key =
304            set_asset_metadata(FungibleAsset::mock(25).vault_key(), AssetComposition::None.as_u8());
305
306        let err =
307            FungibleAsset::from_key_value_words(asset_key, FungibleAsset::mock(5).to_value_word())
308                .unwrap_err();
309        assert_matches!(err, AssetError::AssetCompositionMismatch {
310                faucet_id: _, expected, actual: _
311            } => {
312                assert_eq!(expected, AssetComposition::Fungible);
313        });
314
315        Ok(())
316    }
317
318    #[test]
319    fn fungible_asset_from_key_value_words_fails_on_invalid_asset_id() -> anyhow::Result<()> {
320        let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
321        let mut asset_key = AssetVaultKey::new(
322            AssetId::default(),
323            faucet_id,
324            AssetComposition::Fungible,
325            AssetCallbackFlag::Disabled,
326        )?
327        .to_word();
328        asset_key[0] = Felt::from(1u32);
329        asset_key[1] = Felt::from(2u32);
330
331        let err =
332            FungibleAsset::from_key_value_words(asset_key, FungibleAsset::mock(5).to_value_word())
333                .unwrap_err();
334        assert_matches!(err, AssetError::FungibleAssetIdMustBeZero(_));
335
336        Ok(())
337    }
338
339    #[test]
340    fn fungible_asset_from_key_value_fails_on_invalid_value() -> anyhow::Result<()> {
341        let asset = FungibleAsset::mock(42);
342        let mut invalid_value = asset.to_value_word();
343        invalid_value[2] = Felt::from(5u32);
344
345        let err = FungibleAsset::from_key_value(asset.vault_key(), invalid_value).unwrap_err();
346        assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
347
348        Ok(())
349    }
350
351    #[test]
352    fn test_fungible_asset_serde() -> anyhow::Result<()> {
353        for fungible_account_id in [
354            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
355            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
356            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
357            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
358            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
359        ] {
360            let account_id = AccountId::try_from(fungible_account_id).unwrap();
361            let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
362            assert_eq!(
363                fungible_asset,
364                FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
365            );
366            assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint());
367
368            assert_eq!(
369                fungible_asset,
370                FungibleAsset::from_key_value_words(
371                    fungible_asset.to_key_word(),
372                    fungible_asset.to_value_word()
373                )?
374            )
375        }
376
377        let non_fungible_asset = NonFungibleAsset::mock(&[4]);
378        let err = FungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap_err();
379        assert_matches!(err, DeserializationError::InvalidValue(msg) => {
380            assert!(msg.contains("expected fungible asset composition but found None"));
381        });
382
383        Ok(())
384    }
385
386    #[test]
387    fn test_vault_key_for_fungible_asset() {
388        let asset = FungibleAsset::mock(34);
389
390        assert_eq!(asset.vault_key().faucet_id(), FungibleAsset::mock_issuer());
391        assert_eq!(asset.vault_key().asset_id().prefix().as_canonical_u64(), 0);
392        assert_eq!(asset.vault_key().asset_id().suffix().as_canonical_u64(), 0);
393    }
394}