miden_objects/asset/
fungible.rs

1use alloc::{boxed::Box, string::ToString};
2use core::fmt;
3
4use vm_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
5use vm_processor::DeserializationError;
6
7use super::{is_not_a_non_fungible_asset, AccountType, Asset, AssetError, Felt, Word, ZERO};
8use crate::account::{AccountId, AccountIdPrefix};
9
10// FUNGIBLE ASSET
11// ================================================================================================
12/// A fungible asset.
13///
14/// A fungible asset consists of a faucet ID of the faucet which issued the asset as well as the
15/// asset amount. Asset amount is guaranteed to be 2^63 - 1 or smaller.
16#[derive(Debug, Copy, Clone, PartialEq, Eq)]
17pub struct FungibleAsset {
18    faucet_id: AccountId,
19    amount: u64,
20}
21
22impl FungibleAsset {
23    // CONSTANTS
24    // --------------------------------------------------------------------------------------------
25    /// Specifies a maximum amount value for fungible assets which can be at most a 63-bit value.
26    pub const MAX_AMOUNT: u64 = (1_u64 << 63) - 1;
27
28    /// The serialized size of a [`FungibleAsset`] in bytes.
29    ///
30    /// Currently an account ID (15 bytes) plus an amount (u64).
31    pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::<u64>();
32
33    // CONSTRUCTOR
34    // --------------------------------------------------------------------------------------------
35    /// Returns a fungible asset instantiated with the provided faucet ID and amount.
36    ///
37    /// # Errors
38    /// Returns an error if:
39    /// - The faucet_id is not a valid fungible faucet ID.
40    /// - The provided amount is greater than 2^63 - 1.
41    pub const fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
42        let asset = Self { faucet_id, amount };
43        asset.validate()
44    }
45
46    /// Creates a new [FungibleAsset] without checking its validity.
47    pub(crate) fn new_unchecked(value: Word) -> FungibleAsset {
48        FungibleAsset {
49            faucet_id: AccountId::new_unchecked([value[3], value[2]]),
50            amount: value[0].as_int(),
51        }
52    }
53
54    // PUBLIC ACCESSORS
55    // --------------------------------------------------------------------------------------------
56
57    /// Return ID of the faucet which issued this asset.
58    pub fn faucet_id(&self) -> AccountId {
59        self.faucet_id
60    }
61
62    /// Return ID prefix of the faucet which issued this asset.
63    pub fn faucet_id_prefix(&self) -> AccountIdPrefix {
64        self.faucet_id.prefix()
65    }
66
67    /// Returns the amount of this asset.
68    pub fn amount(&self) -> u64 {
69        self.amount
70    }
71
72    /// Returns true if this and the other assets were issued from the same faucet.
73    pub fn is_from_same_faucet(&self, other: &Self) -> bool {
74        self.faucet_id == other.faucet_id
75    }
76
77    /// Returns the key which is used to store this asset in the account vault.
78    pub fn vault_key(&self) -> Word {
79        Self::vault_key_from_faucet(self.faucet_id)
80    }
81
82    // OPERATIONS
83    // --------------------------------------------------------------------------------------------
84
85    /// Adds two fungible assets together and returns the result.
86    ///
87    /// # Errors
88    /// Returns an error if:
89    /// - The assets were not issued by the same faucet.
90    /// - The total value of assets is greater than or equal to 2^63.
91    #[allow(clippy::should_implement_trait)]
92    pub fn add(self, other: Self) -> Result<Self, AssetError> {
93        if self.faucet_id != other.faucet_id {
94            return Err(AssetError::FungibleAssetInconsistentFaucetIds {
95                original_issuer: self.faucet_id,
96                other_issuer: other.faucet_id,
97            });
98        }
99
100        let amount = self
101            .amount
102            .checked_add(other.amount)
103            .expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64");
104        if amount > Self::MAX_AMOUNT {
105            return Err(AssetError::FungibleAssetAmountTooBig(amount));
106        }
107
108        Ok(Self { faucet_id: self.faucet_id, amount })
109    }
110
111    /// Subtracts the specified amount from this asset and returns the resulting asset.
112    ///
113    /// # Errors
114    /// Returns an error if this asset's amount is smaller than the requested amount.
115    pub fn sub(&mut self, amount: u64) -> Result<Self, AssetError> {
116        self.amount = self.amount.checked_sub(amount).ok_or(
117            AssetError::FungibleAssetAmountNotSufficient {
118                minuend: self.amount,
119                subtrahend: amount,
120            },
121        )?;
122
123        Ok(FungibleAsset { faucet_id: self.faucet_id, amount })
124    }
125
126    // HELPER FUNCTIONS
127    // --------------------------------------------------------------------------------------------
128
129    /// Validates this fungible asset.
130    /// # Errors
131    /// Returns an error if:
132    /// - The faucet_id is not a valid fungible faucet ID.
133    /// - The provided amount is greater than 2^63 - 1.
134    const fn validate(self) -> Result<Self, AssetError> {
135        let account_type = self.faucet_id.account_type();
136        if !matches!(account_type, AccountType::FungibleFaucet) {
137            return Err(AssetError::FungibleFaucetIdTypeMismatch(self.faucet_id));
138        }
139
140        if self.amount > Self::MAX_AMOUNT {
141            return Err(AssetError::FungibleAssetAmountTooBig(self.amount));
142        }
143
144        Ok(self)
145    }
146
147    /// Returns the key which is used to store this asset in the account vault.
148    pub(super) fn vault_key_from_faucet(faucet_id: AccountId) -> Word {
149        let mut key = Word::default();
150        key[2] = faucet_id.suffix();
151        key[3] = faucet_id.prefix().as_felt();
152        key
153    }
154}
155
156impl From<FungibleAsset> for Word {
157    fn from(asset: FungibleAsset) -> Self {
158        let mut result = Word::default();
159        result[0] = Felt::new(asset.amount);
160        result[2] = asset.faucet_id.suffix();
161        result[3] = asset.faucet_id.prefix().as_felt();
162        debug_assert!(is_not_a_non_fungible_asset(result));
163        result
164    }
165}
166
167impl From<FungibleAsset> for Asset {
168    fn from(asset: FungibleAsset) -> Self {
169        Asset::Fungible(asset)
170    }
171}
172
173impl TryFrom<Word> for FungibleAsset {
174    type Error = AssetError;
175
176    fn try_from(value: Word) -> Result<Self, Self::Error> {
177        if value[1] != ZERO {
178            return Err(AssetError::FungibleAssetExpectedZero(value));
179        }
180        let faucet_id = AccountId::try_from([value[3], value[2]])
181            .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
182        let amount = value[0].as_int();
183        Self::new(faucet_id, amount)
184    }
185}
186
187impl fmt::Display for FungibleAsset {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(f, "{:?}", self)
190    }
191}
192
193// SERIALIZATION
194// ================================================================================================
195
196impl Serializable for FungibleAsset {
197    fn write_into<W: ByteWriter>(&self, target: &mut W) {
198        // All assets should serialize their faucet ID at the first position to allow them to be
199        // distinguishable during deserialization.
200        target.write(self.faucet_id);
201        target.write(self.amount);
202    }
203
204    fn get_size_hint(&self) -> usize {
205        self.faucet_id.get_size_hint() + self.amount.get_size_hint()
206    }
207}
208
209impl Deserializable for FungibleAsset {
210    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
211        let faucet_id_prefix: AccountIdPrefix = source.read()?;
212        FungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
213    }
214}
215
216impl FungibleAsset {
217    /// Deserializes a [`FungibleAsset`] from an [`AccountIdPrefix`] and the remaining data from the
218    /// given `source`.
219    pub(super) fn deserialize_with_faucet_id_prefix<R: ByteReader>(
220        faucet_id_prefix: AccountIdPrefix,
221        source: &mut R,
222    ) -> Result<Self, DeserializationError> {
223        // The 8 bytes of the prefix have already been read, so we only need to read the remaining 7
224        // bytes of the account ID's 15 total bytes.
225        let suffix_bytes: [u8; 7] = source.read()?;
226        // Convert prefix back to bytes so we can call the TryFrom<[u8; 15]> impl.
227        let prefix_bytes: [u8; 8] = faucet_id_prefix.into();
228        let mut id_bytes: [u8; 15] = [0; 15];
229        id_bytes[..8].copy_from_slice(&prefix_bytes);
230        id_bytes[8..].copy_from_slice(&suffix_bytes);
231
232        let faucet_id = AccountId::try_from(id_bytes)
233            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
234
235        let amount: u64 = source.read()?;
236        FungibleAsset::new(faucet_id, amount)
237            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
238    }
239}
240
241// TESTS
242// ================================================================================================
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use crate::{
248        account::AccountId,
249        testing::account_id::{
250            ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
251            ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2,
252            ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN,
253        },
254    };
255
256    #[test]
257    fn test_fungible_asset_serde() {
258        for fungible_account_id in [
259            ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN,
260            ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
261            ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1,
262            ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2,
263            ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3,
264        ] {
265            let account_id = AccountId::try_from(fungible_account_id).unwrap();
266            let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
267            assert_eq!(
268                fungible_asset,
269                FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
270            );
271        }
272
273        let account_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3).unwrap();
274        let asset = FungibleAsset::new(account_id, 50).unwrap();
275        let mut asset_bytes = asset.to_bytes();
276        assert_eq!(asset_bytes.len(), asset.get_size_hint());
277        assert_eq!(asset.get_size_hint(), FungibleAsset::SERIALIZED_SIZE);
278
279        let non_fungible_faucet_id =
280            AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap();
281
282        // Set invalid Faucet ID.
283        asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes());
284        let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
285        assert!(matches!(err, DeserializationError::InvalidValue(_)));
286    }
287}