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, AssetError, Word};
6use crate::Felt;
7use crate::account::AccountId;
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    /// 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
45    /// Returns a fungible asset instantiated with the provided faucet ID and amount.
46    ///
47    /// # Errors
48    ///
49    /// Returns an error if:
50    /// - The faucet ID is not a valid fungible faucet ID.
51    /// - The provided amount is greater than [`FungibleAsset::MAX_AMOUNT`].
52    pub fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
53        if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
54            return Err(AssetError::FungibleFaucetIdTypeMismatch(faucet_id));
55        }
56
57        if amount > Self::MAX_AMOUNT {
58            return Err(AssetError::FungibleAssetAmountTooBig(amount));
59        }
60
61        Ok(Self { faucet_id, amount })
62    }
63
64    /// Creates a fungible asset from the provided key and value.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if:
69    /// - The provided key does not contain a valid faucet ID.
70    /// - The provided key's asset ID limbs are not zero.
71    /// - The faucet ID is not a fungible faucet ID.
72    /// - The provided value's amount is greater than [`FungibleAsset::MAX_AMOUNT`] or its three
73    ///   most significant elements are not zero.
74    pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
75        if !key.asset_id().is_empty() {
76            return Err(AssetError::FungibleAssetIdMustBeZero(key.asset_id()));
77        }
78
79        if value[1] != Felt::ZERO || value[2] != Felt::ZERO || value[3] != Felt::ZERO {
80            return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value));
81        }
82
83        Self::new(key.faucet_id(), value[0].as_canonical_u64())
84    }
85
86    /// Creates a fungible asset from the provided key and value.
87    ///
88    /// Prefer [`Self::from_key_value`] for more type safety.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if:
93    /// - The provided key does not contain a valid faucet ID.
94    /// - [`Self::from_key_value`] fails.
95    pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
96        let vault_key = AssetVaultKey::try_from(key)?;
97        Self::from_key_value(vault_key, value)
98    }
99
100    // PUBLIC ACCESSORS
101    // --------------------------------------------------------------------------------------------
102
103    /// Return ID of the faucet which issued this asset.
104    pub fn faucet_id(&self) -> AccountId {
105        self.faucet_id
106    }
107
108    /// Returns the amount of this asset.
109    pub fn amount(&self) -> u64 {
110        self.amount
111    }
112
113    /// Returns true if this and the other assets were issued from the same faucet.
114    pub fn is_from_same_faucet(&self, other: &Self) -> bool {
115        self.faucet_id == other.faucet_id
116    }
117
118    /// Returns the key which is used to store this asset in the account vault.
119    pub fn vault_key(&self) -> AssetVaultKey {
120        AssetVaultKey::new_fungible(self.faucet_id).expect("faucet ID should be of type fungible")
121    }
122
123    /// Returns the asset's key encoded to a [`Word`].
124    pub fn to_key_word(&self) -> Word {
125        self.vault_key().to_word()
126    }
127
128    /// Returns the asset's value encoded to a [`Word`].
129    pub fn to_value_word(&self) -> Word {
130        Word::new([
131            Felt::try_from(self.amount)
132                .expect("fungible asset should only allow amounts that fit into a felt"),
133            Felt::ZERO,
134            Felt::ZERO,
135            Felt::ZERO,
136        ])
137    }
138
139    // OPERATIONS
140    // --------------------------------------------------------------------------------------------
141
142    /// Adds two fungible assets together and returns the result.
143    ///
144    /// # Errors
145    /// Returns an error if:
146    /// - The assets were not issued by the same faucet.
147    /// - The total value of assets is greater than or equal to 2^63.
148    #[allow(clippy::should_implement_trait)]
149    pub fn add(self, other: Self) -> Result<Self, AssetError> {
150        if self.faucet_id != other.faucet_id {
151            return Err(AssetError::FungibleAssetInconsistentFaucetIds {
152                original_issuer: self.faucet_id,
153                other_issuer: other.faucet_id,
154            });
155        }
156
157        let amount = self
158            .amount
159            .checked_add(other.amount)
160            .expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64");
161        if amount > Self::MAX_AMOUNT {
162            return Err(AssetError::FungibleAssetAmountTooBig(amount));
163        }
164
165        Ok(Self { faucet_id: self.faucet_id, amount })
166    }
167
168    /// Subtracts a fungible asset from another and returns the result.
169    ///
170    /// # Errors
171    /// Returns an error if:
172    /// - The assets were not issued by the same faucet.
173    /// - The final amount would be negative.
174    #[allow(clippy::should_implement_trait)]
175    pub fn sub(self, other: Self) -> Result<Self, AssetError> {
176        if self.faucet_id != other.faucet_id {
177            return Err(AssetError::FungibleAssetInconsistentFaucetIds {
178                original_issuer: self.faucet_id,
179                other_issuer: other.faucet_id,
180            });
181        }
182
183        let amount = self.amount.checked_sub(other.amount).ok_or(
184            AssetError::FungibleAssetAmountNotSufficient {
185                minuend: self.amount,
186                subtrahend: other.amount,
187            },
188        )?;
189
190        Ok(FungibleAsset { faucet_id: self.faucet_id, amount })
191    }
192}
193
194impl From<FungibleAsset> for Asset {
195    fn from(asset: FungibleAsset) -> Self {
196        Asset::Fungible(asset)
197    }
198}
199
200impl fmt::Display for FungibleAsset {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        // TODO: Replace with hex representation?
203        write!(f, "{self:?}")
204    }
205}
206
207// SERIALIZATION
208// ================================================================================================
209
210impl Serializable for FungibleAsset {
211    fn write_into<W: ByteWriter>(&self, target: &mut W) {
212        // All assets should serialize their faucet ID at the first position to allow them to be
213        // distinguishable during deserialization.
214        target.write(self.faucet_id);
215        target.write(self.amount);
216    }
217
218    fn get_size_hint(&self) -> usize {
219        self.faucet_id.get_size_hint() + self.amount.get_size_hint()
220    }
221}
222
223impl Deserializable for FungibleAsset {
224    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
225        let faucet_id: AccountId = source.read()?;
226        FungibleAsset::deserialize_with_faucet_id(faucet_id, source)
227    }
228}
229
230impl FungibleAsset {
231    /// Deserializes a [`FungibleAsset`] from an [`AccountId`] and the remaining data from the given
232    /// `source`.
233    pub(super) fn deserialize_with_faucet_id<R: ByteReader>(
234        faucet_id: AccountId,
235        source: &mut R,
236    ) -> Result<Self, DeserializationError> {
237        let amount: u64 = source.read()?;
238        FungibleAsset::new(faucet_id, amount)
239            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
240    }
241}
242
243// TESTS
244// ================================================================================================
245
246#[cfg(test)]
247mod tests {
248    use assert_matches::assert_matches;
249
250    use super::*;
251    use crate::account::AccountId;
252    use crate::testing::account_id::{
253        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
254        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
255        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
256        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
257        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
258        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
259    };
260
261    #[test]
262    fn fungible_asset_from_key_value_words_fails_on_invalid_asset_id() -> anyhow::Result<()> {
263        let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
264        let invalid_key = Word::from([
265            Felt::from(1u32),
266            Felt::from(2u32),
267            faucet_id.suffix(),
268            faucet_id.prefix().as_felt(),
269        ]);
270
271        let err = FungibleAsset::from_key_value_words(
272            invalid_key,
273            FungibleAsset::mock(5).to_value_word(),
274        )
275        .unwrap_err();
276        assert_matches!(err, AssetError::FungibleAssetIdMustBeZero(_));
277
278        Ok(())
279    }
280
281    #[test]
282    fn fungible_asset_from_key_value_fails_on_invalid_value() -> anyhow::Result<()> {
283        let asset = FungibleAsset::mock(42);
284        let mut invalid_value = asset.to_value_word();
285        invalid_value[2] = Felt::from(5u32);
286
287        let err = FungibleAsset::from_key_value(asset.vault_key(), invalid_value).unwrap_err();
288        assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
289
290        Ok(())
291    }
292
293    #[test]
294    fn test_fungible_asset_serde() -> anyhow::Result<()> {
295        for fungible_account_id in [
296            ACCOUNT_ID_PRIVATE_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            let account_id = AccountId::try_from(fungible_account_id).unwrap();
303            let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
304            assert_eq!(
305                fungible_asset,
306                FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
307            );
308            assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint());
309
310            assert_eq!(
311                fungible_asset,
312                FungibleAsset::from_key_value_words(
313                    fungible_asset.to_key_word(),
314                    fungible_asset.to_value_word()
315                )?
316            )
317        }
318
319        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap();
320        let asset = FungibleAsset::new(account_id, 50).unwrap();
321        let mut asset_bytes = asset.to_bytes();
322        assert_eq!(asset_bytes.len(), asset.get_size_hint());
323        assert_eq!(asset.get_size_hint(), FungibleAsset::SERIALIZED_SIZE);
324
325        let non_fungible_faucet_id =
326            AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
327
328        // Set invalid Faucet ID.
329        asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes());
330        let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
331        assert!(matches!(err, DeserializationError::InvalidValue(_)));
332
333        Ok(())
334    }
335
336    #[test]
337    fn test_vault_key_for_fungible_asset() {
338        let asset = FungibleAsset::mock(34);
339
340        assert_eq!(asset.vault_key().faucet_id(), FungibleAsset::mock_issuer());
341        assert_eq!(asset.vault_key().asset_id().prefix().as_canonical_u64(), 0);
342        assert_eq!(asset.vault_key().asset_id().suffix().as_canonical_u64(), 0);
343    }
344}