Skip to main content

multiversx_sc/storage/mappers/token/
token_attributes_mapper.rs

1use core::marker::PhantomData;
2
3use crate::{
4    codec::{NestedDecode, NestedEncode, TopDecode, TopEncode},
5    storage::mappers::{
6        StorageMapperFromAddress,
7        source::{CurrentStorage, StorageAddress},
8    },
9    types::ManagedAddress,
10};
11
12use super::super::StorageMapper;
13use crate::{
14    api::{ErrorApiImpl, ManagedTypeApi, StorageMapperApi},
15    storage::{StorageKey, storage_clear, storage_get, storage_get_len, storage_set},
16    types::{EsdtTokenIdentifier, ManagedType},
17};
18
19const MAPPING_SUFFIX: &str = ".mapping";
20const COUNTER_SUFFIX: &str = ".counter";
21const ATTR_SUFFIX: &str = ".attr";
22const NONCE_SUFFIX: &str = ".nonce";
23
24const VALUE_ALREADY_SET_ERROR_MESSAGE: &str = "A value was already set";
25
26const UNKNOWN_TOKEN_ID_ERROR_MESSAGE: &str = "Unknown token id";
27
28const VALUE_NOT_PREVIOUSLY_SET_ERROR_MESSAGE: &str = "A value was not previously set";
29
30const COUNTER_OVERFLOW_ERROR_MESSAGE: &str =
31    "Counter overflow. This module can hold evidence for maximum u8::MAX different token IDs";
32
33/// Specialized mapper for managing NFT/SFT token attributes with efficient storage
34/// and bidirectional lookup capabilities. Maps token attributes to nonces and vice versa,
35/// enabling efficient queries by both token nonce and attribute values.
36///
37/// # Storage Layout
38///
39/// The mapper uses a sophisticated multi-key storage layout for efficient lookups:
40///
41/// ```text
42/// base_key + ".counter" → u8                    // Global token ID counter
43/// base_key + ".mapping" + token_id → u8         // Token ID to internal mapping ID
44/// base_key + ".attr" + mapping + nonce → T     // Token attributes by nonce
45/// base_key + ".nonce" + mapping + attr → u64   // Nonce lookup by attributes
46/// ```
47///
48/// # Main Operations
49///
50/// ## Attribute Management
51/// - **Set**: Store attributes for token nonce with `set()`
52/// - **Update**: Modify existing attributes with `update()`
53/// - **Clear**: Remove attributes with `clear()`
54/// - **Get**: Retrieve attributes by nonce with `get_attributes()`
55///
56/// ## Bidirectional Lookup
57/// - **Nonce by Attributes**: Find nonce for specific attributes with `get_nonce()`
58/// - **Attributes by Nonce**: Find attributes for specific nonce with `get_attributes()`
59/// - **Existence Checks**: Verify presence with `has_attributes()`, `has_nonce()`
60///
61/// ## Internal Mapping
62/// - **Space Optimization**: Uses u8 internal IDs to reduce storage keys
63/// - **Counter Management**: Automatically assigns mapping IDs up to 255 tokens
64/// - **Collision Avoidance**: Each token ID gets unique mapping space
65///
66/// # Trade-offs
67///
68/// **Advantages:**
69/// - Bidirectional lookup (nonce ↔ attributes) in O(1) time
70/// - Space-efficient storage with internal mapping compression
71/// - Prevents duplicate attribute sets per token
72/// - Supports any encodable attribute type
73///
74/// **Limitations:**
75/// - Maximum 255 different token IDs per mapper instance
76/// - Attributes cannot be changed once set (use update carefully)
77/// - Complex storage layout increases implementation overhead
78/// - No iteration capabilities over stored mappings
79pub struct TokenAttributesMapper<SA, A = CurrentStorage>
80where
81    SA: StorageMapperApi,
82{
83    _phantom_api: PhantomData<SA>,
84    base_key: StorageKey<SA>,
85    address: A,
86}
87
88impl<SA> StorageMapper<SA> for TokenAttributesMapper<SA, CurrentStorage>
89where
90    SA: StorageMapperApi,
91{
92    fn new(base_key: StorageKey<SA>) -> Self {
93        TokenAttributesMapper {
94            _phantom_api: PhantomData,
95            base_key,
96            address: CurrentStorage,
97        }
98    }
99}
100
101impl<SA> StorageMapperFromAddress<SA> for TokenAttributesMapper<SA, ManagedAddress<SA>>
102where
103    SA: StorageMapperApi,
104{
105    fn new_from_address(address: ManagedAddress<SA>, base_key: StorageKey<SA>) -> Self {
106        Self {
107            _phantom_api: PhantomData,
108            base_key,
109            address,
110        }
111    }
112}
113
114impl<SA> TokenAttributesMapper<SA, CurrentStorage>
115where
116    SA: StorageMapperApi,
117{
118    pub fn set<T: TopEncode + TopDecode + NestedEncode + NestedDecode, M: ManagedTypeApi>(
119        &self,
120        token_id: &EsdtTokenIdentifier<M>,
121        token_nonce: u64,
122        attributes: &T,
123    ) {
124        let has_mapping = self.has_mapping_value(token_id);
125
126        let mapping = if has_mapping {
127            self.get_mapping_value(token_id)
128        } else {
129            let mut counter = self.get_counter_value();
130            if counter == u8::MAX {
131                SA::error_api_impl().signal_error(COUNTER_OVERFLOW_ERROR_MESSAGE.as_bytes());
132            }
133
134            counter += 1;
135            self.set_mapping_value(token_id, counter);
136            self.set_counter_value(counter);
137            counter
138        };
139
140        let has_value = self.has_token_attributes_value(mapping, token_nonce);
141        if has_value {
142            SA::error_api_impl().signal_error(VALUE_ALREADY_SET_ERROR_MESSAGE.as_bytes());
143        }
144
145        self.set_token_attributes_value(mapping, token_nonce, attributes);
146        self.set_attributes_to_nonce_mapping(mapping, attributes, token_nonce);
147    }
148
149    ///Use carefully. Update should be used mainly when backed up by the protocol.
150    pub fn update<T: TopEncode + TopDecode + NestedEncode + NestedDecode, M: ManagedTypeApi>(
151        &self,
152        token_id: &EsdtTokenIdentifier<M>,
153        token_nonce: u64,
154        attributes: &T,
155    ) {
156        let has_mapping = self.has_mapping_value(token_id);
157        if !has_mapping {
158            SA::error_api_impl().signal_error(UNKNOWN_TOKEN_ID_ERROR_MESSAGE.as_bytes());
159        }
160
161        let mapping = self.get_mapping_value(token_id);
162        let has_value = self.has_token_attributes_value(mapping, token_nonce);
163        if !has_value {
164            SA::error_api_impl().signal_error(VALUE_NOT_PREVIOUSLY_SET_ERROR_MESSAGE.as_bytes());
165        }
166
167        let old_attr = self.get_token_attributes_value::<T>(mapping, token_nonce);
168        self.clear_attributes_to_nonce_mapping(mapping, &old_attr);
169
170        self.set_token_attributes_value(mapping, token_nonce, attributes);
171        self.set_attributes_to_nonce_mapping(mapping, attributes, token_nonce);
172    }
173
174    pub fn clear<T: TopEncode + TopDecode + NestedEncode + NestedDecode, M: ManagedTypeApi>(
175        &self,
176        token_id: &EsdtTokenIdentifier<M>,
177        token_nonce: u64,
178    ) {
179        let has_mapping = self.has_mapping_value(token_id);
180        if !has_mapping {
181            return;
182        }
183
184        let mapping = self.get_mapping_value(token_id);
185        let has_value = self.has_token_attributes_value(mapping, token_nonce);
186        if !has_value {
187            return;
188        }
189
190        let attr: T = self.get_token_attributes_value(mapping, token_nonce);
191        self.clear_token_attributes_value(mapping, token_nonce);
192        self.clear_attributes_to_nonce_mapping(mapping, &attr);
193    }
194
195    fn set_counter_value(&self, value: u8) {
196        storage_set(self.build_key_token_id_counter().as_ref(), &value);
197    }
198
199    fn set_mapping_value<M: ManagedTypeApi>(&self, token_id: &EsdtTokenIdentifier<M>, value: u8) {
200        storage_set(self.build_key_token_id_mapping(token_id).as_ref(), &value);
201    }
202
203    fn set_attributes_to_nonce_mapping<T: TopEncode + TopDecode + NestedEncode + NestedDecode>(
204        &self,
205        mapping: u8,
206        attr: &T,
207        token_nonce: u64,
208    ) {
209        storage_set(
210            self.build_key_attr_to_nonce_mapping(mapping, attr).as_ref(),
211            &token_nonce,
212        );
213    }
214
215    fn clear_attributes_to_nonce_mapping<T: TopEncode + TopDecode + NestedEncode + NestedDecode>(
216        &self,
217        mapping: u8,
218        attr: &T,
219    ) {
220        storage_clear(self.build_key_attr_to_nonce_mapping(mapping, attr).as_ref());
221    }
222
223    fn set_token_attributes_value<T: TopEncode + TopDecode + NestedEncode + NestedDecode>(
224        &self,
225        mapping: u8,
226        token_nonce: u64,
227        value: &T,
228    ) {
229        storage_set(
230            self.build_key_token_attr_value(mapping, token_nonce)
231                .as_ref(),
232            value,
233        );
234    }
235
236    fn clear_token_attributes_value(&self, mapping: u8, token_nonce: u64) {
237        storage_clear(
238            self.build_key_token_attr_value(mapping, token_nonce)
239                .as_ref(),
240        );
241    }
242}
243
244impl<SA, A> TokenAttributesMapper<SA, A>
245where
246    SA: StorageMapperApi,
247    A: StorageAddress<SA>,
248{
249    pub fn has_attributes<M: ManagedTypeApi>(
250        &self,
251        token_id: &EsdtTokenIdentifier<M>,
252        token_nonce: u64,
253    ) -> bool {
254        let has_mapping = self.has_mapping_value(token_id);
255        if !has_mapping {
256            return true;
257        }
258
259        let mapping = self.get_mapping_value(token_id);
260        self.is_empty_token_attributes_value(mapping, token_nonce)
261    }
262
263    pub fn has_nonce<T: TopEncode + TopDecode + NestedEncode + NestedDecode, M: ManagedTypeApi>(
264        &self,
265        token_id: &EsdtTokenIdentifier<M>,
266        attr: &T,
267    ) -> bool {
268        let has_mapping = self.has_mapping_value(token_id);
269        if !has_mapping {
270            return true;
271        }
272
273        let mapping = self.get_mapping_value(token_id);
274        self.is_empty_attributes_to_nonce_mapping(mapping, attr)
275    }
276
277    pub fn get_attributes<
278        T: TopEncode + TopDecode + NestedEncode + NestedDecode,
279        M: ManagedTypeApi,
280    >(
281        &self,
282        token_id: &EsdtTokenIdentifier<M>,
283        token_nonce: u64,
284    ) -> T {
285        let has_mapping = self.has_mapping_value(token_id);
286        if !has_mapping {
287            SA::error_api_impl().signal_error(UNKNOWN_TOKEN_ID_ERROR_MESSAGE.as_bytes());
288        }
289
290        let mapping = self.get_mapping_value(token_id);
291        let has_value = self.has_token_attributes_value(mapping, token_nonce);
292        if !has_value {
293            SA::error_api_impl().signal_error(VALUE_NOT_PREVIOUSLY_SET_ERROR_MESSAGE.as_bytes());
294        }
295
296        self.get_token_attributes_value(mapping, token_nonce)
297    }
298
299    pub fn get_nonce<T: TopEncode + TopDecode + NestedEncode + NestedDecode, M: ManagedTypeApi>(
300        &self,
301        token_id: &EsdtTokenIdentifier<M>,
302        attr: &T,
303    ) -> u64 {
304        let has_mapping = self.has_mapping_value(token_id);
305        if !has_mapping {
306            SA::error_api_impl().signal_error(UNKNOWN_TOKEN_ID_ERROR_MESSAGE.as_bytes());
307        }
308
309        let mapping = self.get_mapping_value(token_id);
310        let has_value = self.has_attr_to_nonce_mapping::<T>(mapping, attr);
311        if !has_value {
312            SA::error_api_impl().signal_error(VALUE_NOT_PREVIOUSLY_SET_ERROR_MESSAGE.as_bytes());
313        }
314
315        self.get_attributes_to_nonce_mapping(mapping, attr)
316    }
317
318    fn has_mapping_value<M: ManagedTypeApi>(&self, token_id: &EsdtTokenIdentifier<M>) -> bool {
319        !self.is_empty_mapping_value(token_id)
320    }
321
322    fn has_token_attributes_value(&self, mapping: u8, token_nonce: u64) -> bool {
323        !self.is_empty_token_attributes_value(mapping, token_nonce)
324    }
325
326    fn has_attr_to_nonce_mapping<T: TopEncode + TopDecode + NestedEncode + NestedDecode>(
327        &self,
328        mapping: u8,
329        attr: &T,
330    ) -> bool {
331        !self.is_empty_attributes_to_nonce_mapping(mapping, attr)
332    }
333
334    fn build_key_token_id_counter(&self) -> StorageKey<SA> {
335        let mut key = self.base_key.clone();
336        key.append_bytes(COUNTER_SUFFIX.as_bytes());
337        key
338    }
339
340    fn build_key_token_id_mapping<M: ManagedTypeApi>(
341        &self,
342        token_id: &EsdtTokenIdentifier<M>,
343    ) -> StorageKey<SA> {
344        let mut key = self.base_key.clone();
345        key.append_bytes(MAPPING_SUFFIX.as_bytes());
346        key.append_item(token_id);
347        key
348    }
349
350    fn build_key_token_attr_value(&self, mapping: u8, token_nonce: u64) -> StorageKey<SA> {
351        let mut key = self.base_key.clone();
352        key.append_bytes(ATTR_SUFFIX.as_bytes());
353        key.append_item(&mapping);
354        key.append_item(&token_nonce);
355        key
356    }
357
358    fn build_key_attr_to_nonce_mapping<T: TopEncode + TopDecode + NestedEncode + NestedDecode>(
359        &self,
360        mapping: u8,
361        attr: &T,
362    ) -> StorageKey<SA> {
363        let mut key = self.base_key.clone();
364        key.append_bytes(NONCE_SUFFIX.as_bytes());
365        key.append_item(&mapping);
366        key.append_item(attr);
367        key
368    }
369
370    fn get_counter_value(&self) -> u8 {
371        storage_get(self.build_key_token_id_counter().as_ref())
372    }
373
374    fn get_mapping_value<M: ManagedTypeApi>(&self, token_id: &EsdtTokenIdentifier<M>) -> u8 {
375        storage_get(self.build_key_token_id_mapping(token_id).as_ref())
376    }
377
378    fn is_empty_mapping_value<M: ManagedTypeApi>(&self, token_id: &EsdtTokenIdentifier<M>) -> bool {
379        storage_get_len(self.build_key_token_id_mapping(token_id).as_ref()) == 0
380    }
381
382    fn get_attributes_to_nonce_mapping<T: TopEncode + TopDecode + NestedEncode + NestedDecode>(
383        &self,
384        mapping: u8,
385        attr: &T,
386    ) -> u64 {
387        storage_get(self.build_key_attr_to_nonce_mapping(mapping, attr).as_ref())
388    }
389
390    fn is_empty_attributes_to_nonce_mapping<
391        T: TopEncode + TopDecode + NestedEncode + NestedDecode,
392    >(
393        &self,
394        mapping: u8,
395        attr: &T,
396    ) -> bool {
397        storage_get_len(self.build_key_attr_to_nonce_mapping(mapping, attr).as_ref()) == 0
398    }
399
400    fn get_token_attributes_value<T: TopEncode + TopDecode + NestedEncode + NestedDecode>(
401        &self,
402        mapping: u8,
403        token_nonce: u64,
404    ) -> T {
405        storage_get(
406            self.build_key_token_attr_value(mapping, token_nonce)
407                .as_ref(),
408        )
409    }
410
411    fn is_empty_token_attributes_value(&self, mapping: u8, token_nonce: u64) -> bool {
412        storage_get_len(
413            self.build_key_token_attr_value(mapping, token_nonce)
414                .as_ref(),
415        ) == 0
416    }
417}