Skip to main content

miden_protocol/note/
assets.rs

1use alloc::vec::Vec;
2
3use miden_crypto::SequentialCommit;
4
5use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
6use crate::errors::NoteError;
7use crate::utils::serde::{
8    ByteReader,
9    ByteWriter,
10    Deserializable,
11    DeserializationError,
12    Serializable,
13};
14use crate::{Felt, Hasher, MAX_ASSETS_PER_NOTE, WORD_SIZE, Word};
15
16// NOTE ASSETS
17// ================================================================================================
18
19/// An asset container for a note.
20///
21/// A note can contain between 0 and 16 assets. No duplicates are allowed, but the order of assets
22/// is unspecified.
23///
24/// All the assets in a note can be reduced to a single commitment which is computed by
25/// sequentially hashing the assets. Note that the same list of assets can result in two different
26/// commitments if the asset ordering is different.
27#[derive(Debug, Default, Clone)]
28pub struct NoteAssets {
29    assets: Vec<Asset>,
30    commitment: Word,
31}
32
33impl NoteAssets {
34    // CONSTANTS
35    // --------------------------------------------------------------------------------------------
36
37    /// The maximum number of assets which can be carried by a single note.
38    pub const MAX_NUM_ASSETS: usize = MAX_ASSETS_PER_NOTE;
39
40    // CONSTRUCTOR
41    // --------------------------------------------------------------------------------------------
42
43    /// Returns new [NoteAssets] constructed from the provided list of assets.
44    ///
45    /// # Errors
46    /// Returns an error if:
47    /// - The list contains more than 16 assets.
48    /// - There are duplicate assets in the list.
49    pub fn new(assets: Vec<Asset>) -> Result<Self, NoteError> {
50        if assets.len() > Self::MAX_NUM_ASSETS {
51            return Err(NoteError::TooManyAssets(assets.len()));
52        }
53
54        // make sure all provided assets are unique
55        for (i, asset) in assets.iter().enumerate().skip(1) {
56            // for all assets except the first one, check if the asset is the same as any other
57            // asset in the list, and if so return an error
58            if assets[..i].iter().any(|a| a.is_same(asset)) {
59                return Err(match asset {
60                    Asset::Fungible(asset) => NoteError::DuplicateFungibleAsset(asset.faucet_id()),
61                    Asset::NonFungible(asset) => NoteError::DuplicateNonFungibleAsset(*asset),
62                });
63            }
64        }
65
66        let commitment = to_commitment(&assets);
67
68        Ok(Self { assets, commitment })
69    }
70
71    // PUBLIC ACCESSORS
72    // --------------------------------------------------------------------------------------------
73
74    /// Returns a commitment to the note's assets.
75    pub fn commitment(&self) -> Word {
76        self.commitment
77    }
78
79    /// Returns the assets as a slice.
80    pub fn as_slice(&self) -> &[Asset] {
81        &self.assets
82    }
83
84    /// Returns the number of assets.
85    pub fn num_assets(&self) -> usize {
86        self.assets.len()
87    }
88
89    /// Returns true if the number of assets is 0.
90    pub fn is_empty(&self) -> bool {
91        self.assets.is_empty()
92    }
93
94    /// Returns an iterator over all assets.
95    pub fn iter(&self) -> core::slice::Iter<'_, Asset> {
96        self.assets.iter()
97    }
98
99    /// Returns all assets represented as a vector of field elements.
100    pub fn to_elements(&self) -> Vec<Felt> {
101        <Self as SequentialCommit>::to_elements(self)
102    }
103
104    /// Returns an iterator over all [`FungibleAsset`].
105    pub fn iter_fungible(&self) -> impl Iterator<Item = FungibleAsset> {
106        self.assets.iter().filter_map(|asset| match asset {
107            Asset::Fungible(fungible_asset) => Some(*fungible_asset),
108            Asset::NonFungible(_) => None,
109        })
110    }
111
112    /// Returns iterator over all [`NonFungibleAsset`].
113    pub fn iter_non_fungible(&self) -> impl Iterator<Item = NonFungibleAsset> {
114        self.assets.iter().filter_map(|asset| match asset {
115            Asset::Fungible(_) => None,
116            Asset::NonFungible(non_fungible_asset) => Some(*non_fungible_asset),
117        })
118    }
119
120    /// Consumes self and returns the underlying vector of assets.
121    pub fn into_vec(self) -> Vec<Asset> {
122        self.assets
123    }
124}
125
126impl PartialEq for NoteAssets {
127    fn eq(&self, other: &Self) -> bool {
128        self.assets == other.assets
129    }
130}
131
132impl Eq for NoteAssets {}
133
134impl SequentialCommit for NoteAssets {
135    type Commitment = Word;
136
137    /// Returns all assets represented as a vector of field elements.
138    fn to_elements(&self) -> Vec<Felt> {
139        to_elements(&self.assets)
140    }
141
142    /// Computes the commitment to the assets.
143    fn to_commitment(&self) -> Self::Commitment {
144        to_commitment(&self.assets)
145    }
146}
147
148fn to_elements(assets: &[Asset]) -> Vec<Felt> {
149    let mut elements = Vec::with_capacity(assets.len() * 2 * WORD_SIZE);
150    elements.extend(assets.iter().flat_map(Asset::as_elements));
151    elements
152}
153
154fn to_commitment(assets: &[Asset]) -> Word {
155    Hasher::hash_elements(&to_elements(assets))
156}
157
158// SERIALIZATION
159// ================================================================================================
160
161impl Serializable for NoteAssets {
162    fn write_into<W: ByteWriter>(&self, target: &mut W) {
163        const _: () = assert!(NoteAssets::MAX_NUM_ASSETS <= u8::MAX as usize);
164        debug_assert!(self.assets.len() <= NoteAssets::MAX_NUM_ASSETS);
165        target.write_u8(self.assets.len().try_into().expect("Asset number must fit into `u8`"));
166        target.write_many(&self.assets);
167    }
168
169    fn get_size_hint(&self) -> usize {
170        // Size of the serialized asset count prefix.
171        let u8_size = 0u8.get_size_hint();
172
173        let assets_size: usize = self.assets.iter().map(|asset| asset.get_size_hint()).sum();
174
175        u8_size + assets_size
176    }
177}
178
179impl Deserializable for NoteAssets {
180    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
181        let count = source.read_u8()?;
182        let assets = source.read_many_iter::<Asset>(count.into())?.collect::<Result<_, _>>()?;
183        Self::new(assets).map_err(|e| DeserializationError::InvalidValue(format!("{e:?}")))
184    }
185}
186
187// TESTS
188// ================================================================================================
189
190#[cfg(test)]
191mod tests {
192    use alloc::vec;
193    use alloc::vec::Vec;
194
195    use assert_matches::assert_matches;
196
197    use super::NoteAssets;
198    use crate::account::AccountId;
199    use crate::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
200    use crate::errors::NoteError;
201    use crate::testing::account_id::{
202        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
203        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
204        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
205    };
206
207    /// Helper to create `n` unique non-fungible assets.
208    fn make_non_fungible_assets(n: usize) -> Vec<Asset> {
209        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
210        (0..n)
211            .map(|i| {
212                // Use the index bytes to create unique asset data.
213                let data = (i as u64).to_le_bytes().to_vec();
214                let details = NonFungibleAssetDetails::new(faucet_id, data);
215                Asset::NonFungible(NonFungibleAsset::new(&details))
216            })
217            .collect()
218    }
219
220    #[test]
221    fn iter_fungible_asset() {
222        let faucet_id_1 = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
223        let faucet_id_2 = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
224        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
225        let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]);
226
227        let asset1 = Asset::Fungible(FungibleAsset::new(faucet_id_1, 100).unwrap());
228        let asset2 = Asset::Fungible(FungibleAsset::new(faucet_id_2, 50).unwrap());
229        let non_fungible_asset = Asset::NonFungible(NonFungibleAsset::new(&details));
230
231        // Create NoteAsset from assets
232        let assets = NoteAssets::new([asset1, asset2, non_fungible_asset].to_vec()).unwrap();
233
234        let mut fungible_assets = assets.iter_fungible();
235        assert_eq!(fungible_assets.next().unwrap(), asset1.unwrap_fungible());
236        assert_eq!(fungible_assets.next().unwrap(), asset2.unwrap_fungible());
237        assert_eq!(fungible_assets.next(), None);
238    }
239
240    #[test]
241    fn note_assets_at_max_succeeds() {
242        assert_eq!(NoteAssets::MAX_NUM_ASSETS, 16);
243
244        let assets = make_non_fungible_assets(NoteAssets::MAX_NUM_ASSETS);
245        assert_eq!(assets.len(), NoteAssets::MAX_NUM_ASSETS);
246
247        let note_assets = NoteAssets::new(assets).unwrap();
248        assert_eq!(note_assets.num_assets(), NoteAssets::MAX_NUM_ASSETS);
249    }
250
251    #[test]
252    fn note_assets_exceeding_max_fails() {
253        let assets = make_non_fungible_assets(NoteAssets::MAX_NUM_ASSETS + 1);
254        assert_eq!(assets.len(), NoteAssets::MAX_NUM_ASSETS + 1);
255
256        let result = NoteAssets::new(assets);
257        assert_matches!(result, Err(NoteError::TooManyAssets(n)) if n == NoteAssets::MAX_NUM_ASSETS + 1);
258    }
259}