miden_protocol/asset/vault/
vault_key.rs1use alloc::boxed::Box;
2use alloc::string::ToString;
3use core::fmt;
4
5use miden_crypto::merkle::smt::LeafIndex;
6
7use crate::account::AccountId;
8use crate::asset::vault::AssetId;
9use crate::asset::{Asset, AssetCallbackFlag, AssetComposition, FungibleAsset, NonFungibleAsset};
10use crate::crypto::merkle::smt::SMT_DEPTH;
11use crate::errors::AssetError;
12use crate::utils::serde::{
13 ByteReader,
14 ByteWriter,
15 Deserializable,
16 DeserializationError,
17 Serializable,
18};
19use crate::{Felt, Word};
20
21#[derive(Debug, PartialEq, Eq, Clone, Copy)]
37pub struct AssetVaultKey {
38 asset_id: AssetId,
40
41 faucet_id: AccountId,
43
44 composition: AssetComposition,
46
47 callback_flag: AssetCallbackFlag,
49}
50
51impl AssetVaultKey {
52 pub const SERIALIZED_SIZE: usize = Word::SERIALIZED_SIZE;
56
57 pub(in crate::asset) const METADATA_BYTE_MASK: u8 = 0xff;
62
63 pub(in crate::asset) const COMPOSITION_MASK: u8 = 0b11;
67
68 pub(in crate::asset) const CALLBACK_FLAG_MASK: u8 = 0b1 << Self::CALLBACK_FLAG_SHIFT;
70 pub(in crate::asset) const CALLBACK_FLAG_SHIFT: u8 = 2;
71
72 pub(in crate::asset) const METADATA_RESERVED_MASK: u8 = 0b1111_1000;
74
75 pub fn new(
88 asset_id: AssetId,
89 faucet_id: AccountId,
90 composition: AssetComposition,
91 callback_flag: AssetCallbackFlag,
92 ) -> Result<Self, AssetError> {
93 if composition.is_custom() {
95 return Err(AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
96 }
97
98 if composition.is_fungible() && !asset_id.is_empty() {
99 return Err(AssetError::FungibleAssetIdMustBeZero(asset_id));
100 }
101
102 Ok(Self {
103 asset_id,
104 faucet_id,
105 composition,
106 callback_flag,
107 })
108 }
109
110 pub fn new_fungible(faucet_id: AccountId, callback_flag: AssetCallbackFlag) -> Self {
112 Self::new(AssetId::default(), faucet_id, AssetComposition::Fungible, callback_flag).expect(
113 "passing AssetComposition::Fungible together with AssetId::default should be valid",
114 )
115 }
116
117 pub fn to_word(&self) -> Word {
124 let faucet_suffix = self.faucet_id.suffix().as_canonical_u64();
125 debug_assert!(
128 faucet_suffix & Self::METADATA_BYTE_MASK as u64 == 0,
129 "lower 8 bits of faucet suffix must be zero",
130 );
131 let metadata_byte =
132 self.composition.as_u8() | (self.callback_flag.as_u8() << Self::CALLBACK_FLAG_SHIFT);
133 let faucet_id_suffix_and_metadata = faucet_suffix | metadata_byte as u64;
134 let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata)
135 .expect("highest bit should still be zero resulting in a valid felt");
136
137 Word::new([
138 self.asset_id.suffix(),
139 self.asset_id.prefix(),
140 faucet_id_suffix_and_metadata,
141 self.faucet_id.prefix().as_felt(),
142 ])
143 }
144
145 pub fn asset_id(&self) -> AssetId {
148 self.asset_id
149 }
150
151 pub fn faucet_id(&self) -> AccountId {
153 self.faucet_id
154 }
155
156 pub fn callback_flag(&self) -> AssetCallbackFlag {
158 self.callback_flag
159 }
160
161 pub fn composition(&self) -> AssetComposition {
163 self.composition
164 }
165
166 pub fn to_leaf_index(&self) -> LeafIndex<SMT_DEPTH> {
168 LeafIndex::<SMT_DEPTH>::from(self.to_word())
169 }
170}
171
172impl From<AssetVaultKey> for Word {
176 fn from(vault_key: AssetVaultKey) -> Self {
177 vault_key.to_word()
178 }
179}
180
181impl Ord for AssetVaultKey {
182 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
184 self.to_word().cmp(&other.to_word())
185 }
186}
187
188impl PartialOrd for AssetVaultKey {
189 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
190 Some(self.cmp(other))
191 }
192}
193
194impl TryFrom<Word> for AssetVaultKey {
195 type Error = AssetError;
196
197 fn try_from(key: Word) -> Result<Self, Self::Error> {
206 let asset_id_suffix = key[0];
207 let asset_id_prefix = key[1];
208 let faucet_id_suffix_and_metadata = key[2];
209 let faucet_id_prefix = key[3];
210
211 let raw = faucet_id_suffix_and_metadata.as_canonical_u64();
212 let metadata_byte = (raw & Self::METADATA_BYTE_MASK as u64) as u8;
213
214 if metadata_byte & Self::METADATA_RESERVED_MASK != 0 {
216 return Err(AssetError::ReservedAssetMetadata(metadata_byte));
217 }
218
219 let callback_flag = AssetCallbackFlag::try_from(
220 (metadata_byte & Self::CALLBACK_FLAG_MASK) >> Self::CALLBACK_FLAG_SHIFT,
221 )?;
222 let composition = AssetComposition::try_from(metadata_byte & Self::COMPOSITION_MASK)?;
223
224 let faucet_id_suffix = Felt::try_from(raw & !(Self::METADATA_BYTE_MASK as u64))
225 .expect("clearing lower bits should not produce an invalid felt");
226
227 let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix);
228 let faucet_id = AccountId::try_from_elements(faucet_id_suffix, faucet_id_prefix)
229 .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
230
231 Self::new(asset_id, faucet_id, composition, callback_flag)
232 }
233}
234
235impl fmt::Display for AssetVaultKey {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 f.write_str(&self.to_word().to_hex())
238 }
239}
240
241impl From<Asset> for AssetVaultKey {
242 fn from(asset: Asset) -> Self {
243 asset.vault_key()
244 }
245}
246
247impl From<FungibleAsset> for AssetVaultKey {
248 fn from(fungible_asset: FungibleAsset) -> Self {
249 fungible_asset.vault_key()
250 }
251}
252
253impl From<NonFungibleAsset> for AssetVaultKey {
254 fn from(non_fungible_asset: NonFungibleAsset) -> Self {
255 non_fungible_asset.vault_key()
256 }
257}
258
259impl Serializable for AssetVaultKey {
263 fn write_into<W: ByteWriter>(&self, target: &mut W) {
264 self.to_word().write_into(target);
265 }
266
267 fn get_size_hint(&self) -> usize {
268 Self::SERIALIZED_SIZE
269 }
270}
271
272impl Deserializable for AssetVaultKey {
273 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
274 let word: Word = source.read()?;
275 Self::try_from(word).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
276 }
277}
278
279#[cfg(test)]
283mod tests {
284 use assert_matches::assert_matches;
285 use rstest::rstest;
286
287 use super::*;
288 use crate::asset::tests::{asset_metadata, set_asset_metadata};
289 use crate::asset::{AssetCallbackFlag, AssetComposition};
290 use crate::testing::account_id::{
291 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
292 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
293 };
294
295 #[rstest]
296 fn asset_vault_key_word_roundtrip(
297 #[values(AssetCallbackFlag::Disabled, AssetCallbackFlag::Enabled)]
298 callback_flag: AssetCallbackFlag,
299 ) -> anyhow::Result<()> {
300 let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
301 let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?;
302
303 let key = AssetVaultKey::new(
305 AssetId::default(),
306 fungible_faucet,
307 AssetComposition::Fungible,
308 callback_flag,
309 )?;
310 assert_eq!(key.composition(), AssetComposition::Fungible);
311 let roundtripped = AssetVaultKey::try_from(key.to_word())?;
312 assert_eq!(key, roundtripped);
313 assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?);
314
315 let key = AssetVaultKey::new(
317 AssetId::new(Felt::from(42u32), Felt::from(99u32)),
318 nonfungible_faucet,
319 AssetComposition::None,
320 callback_flag,
321 )?;
322 assert_eq!(key.composition(), AssetComposition::None);
323 let roundtripped = AssetVaultKey::try_from(key.to_word())?;
324 assert_eq!(key, roundtripped);
325 assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?);
326
327 Ok(())
328 }
329
330 #[test]
331 fn decoding_word_with_reserved_bits_set_fails() -> anyhow::Result<()> {
332 let key = FungibleAsset::mock(42).vault_key();
333 let valid_metadata = asset_metadata(key);
334 let word = set_asset_metadata(key, valid_metadata | AssetVaultKey::METADATA_RESERVED_MASK);
336
337 let err = AssetVaultKey::try_from(word).unwrap_err();
338 assert_matches!(err, AssetError::ReservedAssetMetadata(_));
339
340 Ok(())
341 }
342
343 #[test]
344 fn decoding_word_with_invalid_composition_value_fails() -> anyhow::Result<()> {
345 let key = FungibleAsset::mock(42).vault_key();
346 let invalid_metadata = AssetVaultKey::COMPOSITION_MASK;
348 let word = set_asset_metadata(key, invalid_metadata);
349
350 let err = AssetVaultKey::try_from(word).unwrap_err();
351 assert_matches!(err, AssetError::UnknownAssetComposition(_));
352
353 Ok(())
354 }
355}