Skip to main content

multiversx_sc/storage/mappers/token/
non_fungible_token_mapper.rs

1use multiversx_chain_core::types::EsdtLocalRole;
2
3use crate::{
4    abi::TypeAbiFrom,
5    codec::{EncodeErrorHandler, TopDecode, TopEncode, TopEncodeMulti, TopEncodeMultiOutput},
6    storage::mappers::{
7        StorageMapperFromAddress,
8        source::{CurrentStorage, StorageAddress},
9    },
10    storage_clear, storage_get, storage_get_len, storage_set,
11    types::{
12        ESDTSystemSCAddress, EgldPayment, FunctionCall, ManagedVec, OriginalResultMarker, Tx,
13        TxScEnv, system_proxy::ESDTSystemSCProxy,
14    },
15};
16
17use super::{
18    super::StorageMapper,
19    TokenMapperState,
20    error::{
21        INVALID_PAYMENT_TOKEN_ERR_MSG, INVALID_TOKEN_ID_ERR_MSG, MUST_SET_TOKEN_ID_ERR_MSG,
22        PENDING_ERR_MSG, TOKEN_ID_ALREADY_SET_ERR_MSG,
23    },
24    fungible_token_mapper::DEFAULT_ISSUE_CALLBACK_NAME,
25};
26use crate::{
27    abi::{TypeAbi, TypeName},
28    api::{CallTypeApi, ErrorApiImpl, StorageMapperApi},
29    contract_base::{BlockchainWrapper, SendWrapper},
30    storage::StorageKey,
31    types::{
32        BigUint, CallbackClosure, EsdtTokenData, EsdtTokenIdentifier, EsdtTokenPayment,
33        EsdtTokenType, ManagedAddress, ManagedBuffer, ManagedType,
34        system_proxy::{
35            MetaTokenProperties, NonFungibleTokenProperties, SemiFungibleTokenProperties,
36        },
37    },
38};
39
40const INVALID_TOKEN_TYPE_ERR_MSG: &str = "Invalid token type for NonFungible issue";
41
42/// High-level mapper for non-fungible, semi-fungible, and meta-fungible ESDT tokens.
43/// Provides comprehensive NFT/SFT lifecycle management including issuance, creation,
44/// minting, burning, and attribute management.
45///
46/// # Storage Layout
47///
48/// The mapper stores the token state at the base key:
49/// - `base_key` → `TokenMapperState<SA>` (NotSet | Pending | Token(EsdtTokenIdentifier))
50///
51/// # Main Operations
52///
53/// ## Token Lifecycle
54/// - **Issue**: Create new NFT/SFT collection via `issue()` or `issue_and_set_all_roles()`
55/// - **Set ID**: Manually set token ID with `set_token_id()` for existing collections
56/// - **Query**: Check token state with `is_empty()`, `get_token_id()`, etc.
57///
58/// ## NFT Operations
59/// - **Create**: Mint new NFTs with `nft_create()` or `nft_create_named()`
60/// - **Add Quantity**: Increase SFT supply with `nft_add_quantity()`
61/// - **Update**: Modify NFT attributes with `nft_update_attributes()`
62/// - **Burn**: Destroy NFT/SFT with `nft_burn()`
63/// - **Transfer**: Send tokens with `send_payment()`
64///
65/// ## Token Management
66/// - **Roles**: Manage collection roles with `set_local_roles()`
67/// - **Balance**: Query token balance with `get_balance()`
68/// - **Metadata**: Retrieve token data with `get_all_token_data()`, `get_token_attributes()`
69///
70/// # Trade-offs
71///
72/// **Advantages:**
73/// - Supports all non-fungible token types (NFT, SFT, MetaFungible)
74/// - Complete NFT lifecycle in one mapper
75/// - Built-in metadata and attribute management
76/// - Automatic nonce handling
77/// - Payment validation utilities
78///
79/// **Limitations:**
80/// - Single collection per mapper instance
81/// - Requires careful callback implementation for issuance
82/// - Token creation requires local roles
83/// - Attribute updates limited by protocol
84pub type IssueCallTo<Api> = Tx<
85    TxScEnv<Api>,
86    (),
87    ESDTSystemSCAddress,
88    EgldPayment<Api>,
89    (),
90    FunctionCall<Api>,
91    OriginalResultMarker<EsdtTokenIdentifier<Api>>,
92>;
93
94pub struct NonFungibleTokenMapper<SA, A = CurrentStorage>
95where
96    SA: StorageMapperApi + CallTypeApi,
97    A: StorageAddress<SA>,
98{
99    key: StorageKey<SA>,
100    token_state: TokenMapperState<SA>,
101    address: A,
102}
103
104impl<SA> StorageMapper<SA> for NonFungibleTokenMapper<SA, CurrentStorage>
105where
106    SA: StorageMapperApi + CallTypeApi,
107{
108    fn new(base_key: StorageKey<SA>) -> Self {
109        Self {
110            token_state: storage_get(base_key.as_ref()),
111            key: base_key,
112            address: CurrentStorage,
113        }
114    }
115}
116
117impl<SA> StorageMapperFromAddress<SA> for NonFungibleTokenMapper<SA, ManagedAddress<SA>>
118where
119    SA: StorageMapperApi + CallTypeApi,
120{
121    fn new_from_address(address: ManagedAddress<SA>, base_key: StorageKey<SA>) -> Self {
122        Self {
123            token_state: storage_get(base_key.as_ref()),
124            key: base_key,
125            address,
126        }
127    }
128}
129
130impl<SA> NonFungibleTokenMapper<SA, CurrentStorage>
131where
132    SA: StorageMapperApi + CallTypeApi,
133{
134    /// Important: If you use custom callback, remember to save the token ID in the callback and clear the mapper in case of error! Clear is unusable outside this specific case.
135    ///
136    /// #[callback]
137    /// fn my_custom_callback(
138    ///     &self,
139    ///     #[call_result] result: ManagedAsyncCallResult<()>,
140    /// ) {
141    ///      match result {
142    ///     ManagedAsyncCallResult::Ok(token_id) => {
143    ///         self.fungible_token_mapper().set_token_id(token_id);
144    ///     },
145    ///     ManagedAsyncCallResult::Err(_) => {
146    ///         self.fungible_token_mapper().clear();
147    ///     },
148    /// }
149    ///
150    /// If you want to use default callbacks, import the default_issue_callbacks::DefaultIssueCallbacksModule from multiversx-sc-modules
151    /// and pass None for the opt_callback argument
152    pub fn issue(
153        &self,
154        token_type: EsdtTokenType,
155        issue_cost: BigUint<SA>,
156        token_display_name: ManagedBuffer<SA>,
157        token_ticker: ManagedBuffer<SA>,
158        num_decimals: usize,
159        opt_callback: Option<CallbackClosure<SA>>,
160    ) -> ! {
161        self.check_not_set();
162
163        let callback = match opt_callback {
164            Some(cb) => cb,
165            None => self.default_callback_closure_obj(),
166        };
167        let contract_call = match token_type {
168            EsdtTokenType::NonFungible => {
169                Self::nft_issue(issue_cost, token_display_name, token_ticker)
170            }
171            EsdtTokenType::SemiFungible => {
172                Self::sft_issue(issue_cost, token_display_name, token_ticker)
173            }
174            EsdtTokenType::MetaFungible => {
175                Self::meta_issue(issue_cost, token_display_name, token_ticker, num_decimals)
176            }
177            _ => SA::error_api_impl().signal_error(INVALID_TOKEN_TYPE_ERR_MSG.as_bytes()),
178        };
179
180        storage_set(self.get_storage_key(), &TokenMapperState::<SA>::Pending);
181        contract_call.with_callback(callback).async_call_and_exit();
182    }
183
184    /// Important: If you use custom callback, remember to save the token ID in the callback and clear the mapper in case of error! Clear is unusable outside this specific case.
185    ///
186    /// #[callback]
187    /// fn my_custom_callback(
188    ///     &self,
189    ///     #[call_result] result: ManagedAsyncCallResult<()>,
190    /// ) {
191    ///      match result {
192    ///     ManagedAsyncCallResult::Ok(token_id) => {
193    ///         self.fungible_token_mapper().set_token_id(token_id);
194    ///     },
195    ///     ManagedAsyncCallResult::Err(_) => {
196    ///         self.fungible_token_mapper().clear();
197    ///     },
198    /// }
199    ///
200    /// If you want to use default callbacks, import the default_issue_callbacks::DefaultIssueCallbacksModule from multiversx-sc-modules
201    /// and pass None for the opt_callback argument
202    pub fn issue_and_set_all_roles(
203        &self,
204        token_type: EsdtTokenType,
205        issue_cost: BigUint<SA>,
206        token_display_name: ManagedBuffer<SA>,
207        token_ticker: ManagedBuffer<SA>,
208        num_decimals: usize,
209        opt_callback: Option<CallbackClosure<SA>>,
210    ) -> ! {
211        self.check_not_set();
212
213        if token_type == EsdtTokenType::Fungible || token_type == EsdtTokenType::Invalid {
214            SA::error_api_impl().signal_error(INVALID_TOKEN_TYPE_ERR_MSG.as_bytes());
215        }
216
217        let callback = match opt_callback {
218            Some(cb) => cb,
219            None => self.default_callback_closure_obj(),
220        };
221
222        storage_set(self.get_storage_key(), &TokenMapperState::<SA>::Pending);
223        Tx::new_tx_from_sc()
224            .to(ESDTSystemSCAddress)
225            .typed(ESDTSystemSCProxy)
226            .issue_and_set_all_roles(
227                issue_cost,
228                token_display_name,
229                token_ticker,
230                token_type,
231                num_decimals,
232            )
233            .callback(callback)
234            .async_call_and_exit()
235    }
236
237    pub fn clear(&mut self) {
238        let state: TokenMapperState<SA> = storage_get(self.key.as_ref());
239        if state.is_pending() {
240            storage_clear(self.key.as_ref());
241        }
242    }
243
244    pub fn nft_issue(
245        issue_cost: BigUint<SA>,
246        token_display_name: ManagedBuffer<SA>,
247        token_ticker: ManagedBuffer<SA>,
248    ) -> IssueCallTo<SA> {
249        Tx::new_tx_from_sc()
250            .to(ESDTSystemSCAddress)
251            .typed(ESDTSystemSCProxy)
252            .issue_non_fungible(
253                issue_cost,
254                &token_display_name,
255                &token_ticker,
256                NonFungibleTokenProperties::default(),
257            )
258    }
259
260    pub fn sft_issue(
261        issue_cost: BigUint<SA>,
262        token_display_name: ManagedBuffer<SA>,
263        token_ticker: ManagedBuffer<SA>,
264    ) -> IssueCallTo<SA> {
265        Tx::new_tx_from_sc()
266            .to(ESDTSystemSCAddress)
267            .typed(ESDTSystemSCProxy)
268            .issue_semi_fungible(
269                issue_cost,
270                &token_display_name,
271                &token_ticker,
272                SemiFungibleTokenProperties::default(),
273            )
274    }
275
276    pub fn meta_issue(
277        issue_cost: BigUint<SA>,
278        token_display_name: ManagedBuffer<SA>,
279        token_ticker: ManagedBuffer<SA>,
280        num_decimals: usize,
281    ) -> IssueCallTo<SA> {
282        let properties = MetaTokenProperties {
283            num_decimals,
284            ..Default::default()
285        };
286
287        Tx::new_tx_from_sc()
288            .to(ESDTSystemSCAddress)
289            .typed(ESDTSystemSCProxy)
290            .register_meta_esdt(issue_cost, &token_display_name, &token_ticker, properties)
291    }
292
293    pub fn nft_create<T: TopEncode>(
294        &self,
295        amount: BigUint<SA>,
296        attributes: &T,
297    ) -> EsdtTokenPayment<SA> {
298        let send_wrapper = SendWrapper::<SA>::new();
299        let token_id = self.get_token_id();
300
301        let token_nonce = send_wrapper.esdt_nft_create_compact(&token_id, &amount, attributes);
302
303        EsdtTokenPayment::new(token_id, token_nonce, amount)
304    }
305
306    pub fn nft_create_named<T: TopEncode>(
307        &self,
308        amount: BigUint<SA>,
309        name: &ManagedBuffer<SA>,
310        attributes: &T,
311    ) -> EsdtTokenPayment<SA> {
312        let send_wrapper = SendWrapper::<SA>::new();
313        let token_id = self.get_token_id();
314
315        let token_nonce =
316            send_wrapper.esdt_nft_create_compact_named(&token_id, &amount, name, attributes);
317
318        EsdtTokenPayment::new(token_id, token_nonce, amount)
319    }
320
321    pub fn nft_create_and_send<T: TopEncode>(
322        &self,
323        to: &ManagedAddress<SA>,
324        amount: BigUint<SA>,
325        attributes: &T,
326    ) -> EsdtTokenPayment<SA> {
327        let payment = self.nft_create(amount, attributes);
328        self.send_payment(to, &payment);
329
330        payment
331    }
332
333    pub fn nft_create_and_send_named<T: TopEncode>(
334        &self,
335        to: &ManagedAddress<SA>,
336        amount: BigUint<SA>,
337        name: &ManagedBuffer<SA>,
338        attributes: &T,
339    ) -> EsdtTokenPayment<SA> {
340        let payment = self.nft_create_named(amount, name, attributes);
341        self.send_payment(to, &payment);
342
343        payment
344    }
345
346    pub fn nft_add_quantity(&self, token_nonce: u64, amount: BigUint<SA>) -> EsdtTokenPayment<SA> {
347        let send_wrapper = SendWrapper::<SA>::new();
348        let token_id = self.get_token_id();
349
350        send_wrapper.esdt_local_mint(&token_id, token_nonce, &amount);
351
352        EsdtTokenPayment::new(token_id, token_nonce, amount)
353    }
354
355    pub fn nft_add_quantity_and_send(
356        &self,
357        to: &ManagedAddress<SA>,
358        token_nonce: u64,
359        amount: BigUint<SA>,
360    ) -> EsdtTokenPayment<SA> {
361        let payment = self.nft_add_quantity(token_nonce, amount);
362        self.send_payment(to, &payment);
363
364        payment
365    }
366
367    pub fn nft_update_attributes<T: TopEncode>(&self, token_nonce: u64, new_attributes: &T) {
368        let send_wrapper = SendWrapper::<SA>::new();
369        let token_id = self.get_token_id_ref();
370        send_wrapper.nft_update_attributes(token_id, token_nonce, new_attributes);
371    }
372
373    pub fn nft_burn(&self, token_nonce: u64, amount: &BigUint<SA>) {
374        let send_wrapper = SendWrapper::<SA>::new();
375        let token_id = self.get_token_id_ref();
376
377        send_wrapper.esdt_local_burn(token_id, token_nonce, amount);
378    }
379
380    pub fn send_payment(&self, to: &ManagedAddress<SA>, payment: &EsdtTokenPayment<SA>) {
381        Tx::new_tx_from_sc()
382            .to(to)
383            .single_esdt(
384                &payment.token_identifier,
385                payment.token_nonce,
386                &payment.amount,
387            )
388            .transfer();
389    }
390
391    pub fn set_token_id(&mut self, token_id: EsdtTokenIdentifier<SA>) {
392        self.store_token_id(&token_id);
393        self.token_state = TokenMapperState::Token(token_id);
394    }
395
396    pub fn set_if_empty(&mut self, token_id: EsdtTokenIdentifier<SA>) {
397        if self.is_empty() {
398            self.set_token_id(token_id);
399        }
400    }
401
402    pub fn set_local_roles(
403        &self,
404        roles: &[EsdtLocalRole],
405        opt_callback: Option<CallbackClosure<SA>>,
406    ) -> ! {
407        let own_sc_address = Self::get_sc_address();
408        self.set_local_roles_for_address(&own_sc_address, roles, opt_callback);
409    }
410
411    pub fn set_local_roles_for_address(
412        &self,
413        address: &ManagedAddress<SA>,
414        roles: &[EsdtLocalRole],
415        opt_callback: Option<CallbackClosure<SA>>,
416    ) -> ! {
417        self.require_issued_or_set();
418
419        let token_id = self.get_token_id_ref();
420        Tx::new_tx_from_sc()
421            .to(ESDTSystemSCAddress)
422            .typed(ESDTSystemSCProxy)
423            .set_special_roles(address, token_id, roles[..].iter().cloned())
424            .callback(opt_callback)
425            .async_call_and_exit()
426    }
427
428    pub(crate) fn store_token_id(&self, token_id: &EsdtTokenIdentifier<SA>) {
429        if self.get_token_state().is_set() {
430            SA::error_api_impl().signal_error(TOKEN_ID_ALREADY_SET_ERR_MSG);
431        }
432        if !token_id.is_valid_esdt_identifier() {
433            SA::error_api_impl().signal_error(INVALID_TOKEN_ID_ERR_MSG);
434        }
435        storage_set(
436            self.get_storage_key(),
437            &TokenMapperState::Token(token_id.clone()),
438        );
439    }
440
441    pub fn get_balance(&self, token_nonce: u64) -> BigUint<SA> {
442        let b_wrapper = BlockchainWrapper::new();
443        let own_sc_address = Self::get_sc_address();
444        let token_id = self.get_token_id_ref();
445
446        b_wrapper.get_esdt_balance(&own_sc_address, token_id, token_nonce)
447    }
448
449    pub fn get_sc_address() -> ManagedAddress<SA> {
450        let b_wrapper = BlockchainWrapper::new();
451        b_wrapper.get_sc_address()
452    }
453
454    pub fn get_all_token_data(&self, token_nonce: u64) -> EsdtTokenData<SA> {
455        let b_wrapper = BlockchainWrapper::new();
456        let own_sc_address = Self::get_sc_address();
457        let token_id = self.get_token_id_ref();
458
459        b_wrapper.get_esdt_token_data(&own_sc_address, token_id, token_nonce)
460    }
461
462    pub fn get_token_attributes<T: TopDecode>(&self, token_nonce: u64) -> T {
463        let token_data = self.get_all_token_data(token_nonce);
464        token_data.decode_attributes()
465    }
466}
467
468impl<SA, A> NonFungibleTokenMapper<SA, A>
469where
470    SA: StorageMapperApi + CallTypeApi,
471    A: StorageAddress<SA>,
472{
473    pub(crate) fn check_not_set(&self) {
474        let storage_value: TokenMapperState<SA> = storage_get(self.get_storage_key());
475        match storage_value {
476            TokenMapperState::NotSet => {}
477            TokenMapperState::Pending => {
478                SA::error_api_impl().signal_error(PENDING_ERR_MSG);
479            }
480            TokenMapperState::Token(_) => {
481                SA::error_api_impl().signal_error(TOKEN_ID_ALREADY_SET_ERR_MSG);
482            }
483        }
484    }
485
486    pub fn is_empty(&self) -> bool {
487        storage_get_len(self.get_storage_key()) == 0
488    }
489
490    pub fn require_issued_or_set(&self) {
491        if self.is_empty() {
492            SA::error_api_impl().signal_error(MUST_SET_TOKEN_ID_ERR_MSG);
493        }
494    }
495
496    pub fn require_same_token(&self, expected_token_id: &EsdtTokenIdentifier<SA>) {
497        let actual_token_id = self.get_token_id_ref();
498        if actual_token_id != expected_token_id {
499            SA::error_api_impl().signal_error(INVALID_PAYMENT_TOKEN_ERR_MSG);
500        }
501    }
502
503    pub fn require_all_same_token(&self, payments: &ManagedVec<SA, EsdtTokenPayment<SA>>) {
504        let actual_token_id = self.get_token_id_ref();
505        for p in payments {
506            if actual_token_id != &p.token_identifier {
507                SA::error_api_impl().signal_error(INVALID_PAYMENT_TOKEN_ERR_MSG);
508            }
509        }
510    }
511
512    pub fn get_storage_key(&self) -> crate::types::ManagedRef<'_, SA, StorageKey<SA>> {
513        self.key.as_ref()
514    }
515
516    pub fn get_token_state(&self) -> TokenMapperState<SA> {
517        self.token_state.clone()
518    }
519
520    pub fn get_token_id(&self) -> EsdtTokenIdentifier<SA> {
521        if let TokenMapperState::Token(token) = &self.token_state {
522            token.clone()
523        } else {
524            SA::error_api_impl().signal_error(INVALID_TOKEN_ID_ERR_MSG);
525        }
526    }
527
528    pub fn get_token_id_ref(&self) -> &EsdtTokenIdentifier<SA> {
529        if let TokenMapperState::Token(token) = &self.token_state {
530            token
531        } else {
532            SA::error_api_impl().signal_error(INVALID_TOKEN_ID_ERR_MSG);
533        }
534    }
535
536    pub fn default_callback_closure_obj(&self) -> CallbackClosure<SA> {
537        let initial_caller = BlockchainWrapper::<SA>::new().get_caller();
538        let cb_name = DEFAULT_ISSUE_CALLBACK_NAME;
539
540        let mut cb_closure = CallbackClosure::new(cb_name);
541        cb_closure.push_endpoint_arg(&initial_caller);
542        cb_closure.push_endpoint_arg(&self.key.buffer);
543
544        cb_closure
545    }
546}
547
548impl<SA> TopEncodeMulti for NonFungibleTokenMapper<SA>
549where
550    SA: StorageMapperApi + CallTypeApi,
551{
552    fn multi_encode_or_handle_err<O, H>(&self, output: &mut O, h: H) -> Result<(), H::HandledErr>
553    where
554        O: TopEncodeMultiOutput,
555        H: EncodeErrorHandler,
556    {
557        if self.is_empty() {
558            output.push_single_value(&ManagedBuffer::<SA>::new(), h)
559        } else {
560            output.push_single_value(&self.get_token_id(), h)
561        }
562    }
563}
564
565impl<SA> TypeAbiFrom<NonFungibleTokenMapper<SA>> for EsdtTokenIdentifier<SA> where
566    SA: StorageMapperApi + CallTypeApi
567{
568}
569
570impl<SA> TypeAbiFrom<Self> for NonFungibleTokenMapper<SA> where SA: StorageMapperApi + CallTypeApi {}
571
572impl<SA> TypeAbi for NonFungibleTokenMapper<SA>
573where
574    SA: StorageMapperApi + CallTypeApi,
575{
576    type Unmanaged = Self;
577
578    fn type_name() -> TypeName {
579        EsdtTokenIdentifier::<SA>::type_name()
580    }
581
582    fn type_name_rust() -> TypeName {
583        EsdtTokenIdentifier::<SA>::type_name_rust()
584    }
585
586    fn provide_type_descriptions<TDC: crate::abi::TypeDescriptionContainer>(accumulator: &mut TDC) {
587        EsdtTokenIdentifier::<SA>::provide_type_descriptions(accumulator);
588    }
589
590    fn is_variadic() -> bool {
591        false
592    }
593}