Skip to main content

miden_protocol/asset/vault/
vault_key.rs

1use alloc::boxed::Box;
2use alloc::string::ToString;
3use core::fmt;
4
5use miden_crypto::merkle::smt::LeafIndex;
6
7use crate::account::AccountId;
8use crate::account::AccountType::{self};
9use crate::asset::vault::AssetId;
10use crate::asset::{Asset, AssetCallbackFlag, FungibleAsset, NonFungibleAsset};
11use crate::crypto::merkle::smt::SMT_DEPTH;
12use crate::errors::AssetError;
13use crate::utils::serde::{
14    ByteReader,
15    ByteWriter,
16    Deserializable,
17    DeserializationError,
18    Serializable,
19};
20use crate::{Felt, Word};
21
22/// The unique identifier of an [`Asset`] in the [`AssetVault`](crate::asset::AssetVault).
23///
24/// Its [`Word`] layout is:
25/// ```text
26/// [
27///   asset_id_suffix (64 bits),
28///   asset_id_prefix (64 bits),
29///   [faucet_id_suffix (56 bits) | 7 zero bits | callbacks_enabled (1 bit)],
30///   faucet_id_prefix (64 bits)
31/// ]
32/// ```
33#[derive(Debug, PartialEq, Eq, Clone, Copy)]
34pub struct AssetVaultKey {
35    /// The asset ID of the vault key.
36    asset_id: AssetId,
37
38    /// The ID of the faucet that issued the asset.
39    faucet_id: AccountId,
40
41    /// Determines whether callbacks are enabled.
42    callback_flag: AssetCallbackFlag,
43}
44
45impl AssetVaultKey {
46    /// The serialized size of an [`AssetVaultKey`] in bytes.
47    ///
48    /// Serialized as its [`Word`] representation (4 field elements).
49    pub const SERIALIZED_SIZE: usize = Word::SERIALIZED_SIZE;
50
51    // CONSTRUCTORS
52    // --------------------------------------------------------------------------------------------
53
54    /// Creates an [`AssetVaultKey`] for a native asset with callbacks disabled.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if:
59    /// - the provided ID is not of type
60    ///   [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet) or
61    ///   [`AccountType::NonFungibleFaucet`](crate::account::AccountType::NonFungibleFaucet)
62    /// - the asset ID limbs are not zero when `faucet_id` is of type
63    ///   [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet).
64    pub fn new_native(asset_id: AssetId, faucet_id: AccountId) -> Result<Self, AssetError> {
65        Self::new(asset_id, faucet_id, AssetCallbackFlag::Disabled)
66    }
67
68    /// Creates an [`AssetVaultKey`] from its parts with the given [`AssetCallbackFlag`].
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if:
73    /// - the provided ID is not of type
74    ///   [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet) or
75    ///   [`AccountType::NonFungibleFaucet`](crate::account::AccountType::NonFungibleFaucet)
76    /// - the asset ID limbs are not zero when `faucet_id` is of type
77    ///   [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet).
78    pub fn new(
79        asset_id: AssetId,
80        faucet_id: AccountId,
81        callback_flag: AssetCallbackFlag,
82    ) -> Result<Self, AssetError> {
83        if !faucet_id.is_faucet() {
84            return Err(AssetError::InvalidFaucetAccountId(Box::from(format!(
85                "expected account ID of type faucet, found account type {}",
86                faucet_id.account_type()
87            ))));
88        }
89
90        if matches!(faucet_id.account_type(), AccountType::FungibleFaucet) && !asset_id.is_empty() {
91            return Err(AssetError::FungibleAssetIdMustBeZero(asset_id));
92        }
93
94        Ok(Self { asset_id, faucet_id, callback_flag })
95    }
96
97    // PUBLIC ACCESSORS
98    // --------------------------------------------------------------------------------------------
99
100    /// Returns the word representation of the vault key.
101    ///
102    /// See the type-level documentation for details.
103    pub fn to_word(&self) -> Word {
104        let faucet_suffix = self.faucet_id.suffix().as_canonical_u64();
105        // The lower 8 bits of the faucet suffix are guaranteed to be zero and so it is used to
106        // encode the asset metadata.
107        debug_assert!(faucet_suffix & 0xff == 0, "lower 8 bits of faucet suffix must be zero");
108        let faucet_id_suffix_and_metadata = faucet_suffix | self.callback_flag.as_u8() as u64;
109        let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata)
110            .expect("highest bit should still be zero resulting in a valid felt");
111
112        Word::new([
113            self.asset_id.suffix(),
114            self.asset_id.prefix(),
115            faucet_id_suffix_and_metadata,
116            self.faucet_id.prefix().as_felt(),
117        ])
118    }
119
120    /// Returns the [`AssetId`] of the vault key that distinguishes different assets issued by the
121    /// same faucet.
122    pub fn asset_id(&self) -> AssetId {
123        self.asset_id
124    }
125
126    /// Returns the [`AccountId`] of the faucet that issued the asset.
127    pub fn faucet_id(&self) -> AccountId {
128        self.faucet_id
129    }
130
131    /// Returns the [`AssetCallbackFlag`] flag of the vault key.
132    pub fn callback_flag(&self) -> AssetCallbackFlag {
133        self.callback_flag
134    }
135
136    /// Constructs a fungible asset's key from a faucet ID.
137    ///
138    /// Returns `None` if the provided ID is not of type
139    /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet)
140    pub fn new_fungible(faucet_id: AccountId) -> Option<Self> {
141        if matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
142            let asset_id = AssetId::new(Felt::ZERO, Felt::ZERO);
143            Some(
144                Self::new_native(asset_id, faucet_id)
145                    .expect("we should have account type fungible faucet"),
146            )
147        } else {
148            None
149        }
150    }
151
152    /// Returns the leaf index of a vault key.
153    pub fn to_leaf_index(&self) -> LeafIndex<SMT_DEPTH> {
154        LeafIndex::<SMT_DEPTH>::from(self.to_word())
155    }
156}
157
158// CONVERSIONS
159// ================================================================================================
160
161impl From<AssetVaultKey> for Word {
162    fn from(vault_key: AssetVaultKey) -> Self {
163        vault_key.to_word()
164    }
165}
166
167impl Ord for AssetVaultKey {
168    /// Implements comparison based on the [`Word`] representation.
169    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
170        self.to_word().cmp(&other.to_word())
171    }
172}
173
174impl PartialOrd for AssetVaultKey {
175    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
176        Some(self.cmp(other))
177    }
178}
179
180impl TryFrom<Word> for AssetVaultKey {
181    type Error = AssetError;
182
183    /// Attempts to convert the provided [`Word`] into an [`AssetVaultKey`].
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if:
188    /// - the faucet ID in the key is invalid or not of a faucet type.
189    /// - the asset ID limbs are not zero when `faucet_id` is of type
190    ///   [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet).
191    fn try_from(key: Word) -> Result<Self, Self::Error> {
192        let asset_id_suffix = key[0];
193        let asset_id_prefix = key[1];
194        let faucet_id_suffix_and_metadata = key[2];
195        let faucet_id_prefix = key[3];
196
197        let raw = faucet_id_suffix_and_metadata.as_canonical_u64();
198        let callback_flag = AssetCallbackFlag::try_from((raw & 0xff) as u8)?;
199        let faucet_id_suffix = Felt::try_from(raw & 0xffff_ffff_ffff_ff00)
200            .expect("clearing lower bits should not produce an invalid felt");
201
202        let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix);
203        let faucet_id = AccountId::try_from_elements(faucet_id_suffix, faucet_id_prefix)
204            .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
205
206        Self::new(asset_id, faucet_id, callback_flag)
207    }
208}
209
210impl fmt::Display for AssetVaultKey {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        f.write_str(&self.to_word().to_hex())
213    }
214}
215
216impl From<Asset> for AssetVaultKey {
217    fn from(asset: Asset) -> Self {
218        asset.vault_key()
219    }
220}
221
222impl From<FungibleAsset> for AssetVaultKey {
223    fn from(fungible_asset: FungibleAsset) -> Self {
224        fungible_asset.vault_key()
225    }
226}
227
228impl From<NonFungibleAsset> for AssetVaultKey {
229    fn from(non_fungible_asset: NonFungibleAsset) -> Self {
230        non_fungible_asset.vault_key()
231    }
232}
233
234// SERIALIZATION
235// ================================================================================================
236
237impl Serializable for AssetVaultKey {
238    fn write_into<W: ByteWriter>(&self, target: &mut W) {
239        self.to_word().write_into(target);
240    }
241
242    fn get_size_hint(&self) -> usize {
243        Self::SERIALIZED_SIZE
244    }
245}
246
247impl Deserializable for AssetVaultKey {
248    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
249        let word: Word = source.read()?;
250        Self::try_from(word).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
251    }
252}
253
254// TESTS
255// ================================================================================================
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::asset::AssetCallbackFlag;
261    use crate::testing::account_id::{
262        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
263        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
264    };
265
266    #[test]
267    fn asset_vault_key_word_roundtrip() -> anyhow::Result<()> {
268        let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
269        let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?;
270
271        for callback_flag in [AssetCallbackFlag::Disabled, AssetCallbackFlag::Enabled] {
272            // Fungible: asset_id must be zero.
273            let key = AssetVaultKey::new(AssetId::default(), fungible_faucet, callback_flag)?;
274
275            let roundtripped = AssetVaultKey::try_from(key.to_word())?;
276            assert_eq!(key, roundtripped);
277            assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?);
278
279            // Non-fungible: asset_id can be non-zero.
280            let key = AssetVaultKey::new(
281                AssetId::new(Felt::from(42u32), Felt::from(99u32)),
282                nonfungible_faucet,
283                callback_flag,
284            )?;
285
286            let roundtripped = AssetVaultKey::try_from(key.to_word())?;
287            assert_eq!(key, roundtripped);
288            assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?);
289        }
290
291        Ok(())
292    }
293}