Skip to main content

multiversx_sc/storage/mappers/token/
fungible_token_mapper.rs

1use multiversx_chain_core::types::EsdtLocalRole;
2
3use crate::{
4    abi::{TypeAbi, TypeAbiFrom},
5    api::ErrorApiImpl,
6    codec::{EncodeErrorHandler, TopEncodeMulti, TopEncodeMultiOutput},
7    storage::mappers::{
8        StorageMapperFromAddress,
9        source::{CurrentStorage, StorageAddress},
10    },
11    storage_clear, storage_get, storage_get_len, storage_set,
12    types::{
13        ESDTSystemSCAddress, ManagedRef, Tx,
14        system_proxy::{ESDTSystemSCProxy, FungibleTokenProperties},
15    },
16};
17
18use super::{
19    super::StorageMapper,
20    TokenMapperState,
21    error::{
22        INVALID_PAYMENT_TOKEN_ERR_MSG, INVALID_TOKEN_ID_ERR_MSG, MUST_SET_TOKEN_ID_ERR_MSG,
23        PENDING_ERR_MSG, TOKEN_ID_ALREADY_SET_ERR_MSG,
24    },
25};
26use crate::{
27    abi::TypeName,
28    api::{CallTypeApi, StorageMapperApi},
29    contract_base::{BlockchainWrapper, SendWrapper},
30    storage::StorageKey,
31    types::{
32        BigUint, CallbackClosure, EsdtTokenIdentifier, EsdtTokenPayment, EsdtTokenType,
33        ManagedAddress, ManagedBuffer, ManagedType, ManagedVec,
34    },
35};
36
37pub(crate) const DEFAULT_ISSUE_CALLBACK_NAME: &str = "default_issue_cb";
38pub(crate) const DEFAULT_ISSUE_WITH_INIT_SUPPLY_CALLBACK_NAME: &str =
39    "default_issue_init_supply_cb";
40
41/// High-level mapper for fungible ESDT tokens, providing token issuance, minting, burning,
42/// and management operations. This mapper handles the complete lifecycle of fungible tokens
43/// from issuance to day-to-day operations.
44///
45/// # Storage Layout
46///
47/// The mapper stores the token state at the base key:
48/// - `base_key` → `TokenMapperState<SA>` (NotSet | Pending | Token(EsdtTokenIdentifier))
49///
50/// # Main Operations
51///
52/// ## Token Lifecycle
53/// - **Issue**: Create new fungible token via `issue()` or `issue_and_set_all_roles()`
54/// - **Set ID**: Manually set token ID with `set_token_id()` for existing tokens
55/// - **Query**: Check token state with `is_empty()`, `get_token_id()`, etc.
56///
57/// ## Token Operations
58/// - **Mint**: Create new token supply with `mint()` or `mint_and_send()`
59/// - **Burn**: Destroy token supply with `burn()`
60/// - **Transfer**: Send tokens with `send_payment()`
61/// - **Roles**: Manage token roles with `set_local_roles()`
62///
63/// ## Balance Management
64/// - **Query Balance**: Check contract's balance with `get_balance()`
65/// - **Payment Validation**: Ensure payments match expected token
66///
67/// # Trade-offs
68///
69/// **Advantages:**
70/// - Complete token lifecycle management in one mapper
71/// - Built-in async callback handling for issuance
72/// - Payment validation and role management
73/// - Automatic error handling for invalid states
74///
75/// **Limitations:**
76/// - Single token per mapper instance
77/// - Requires careful callback implementation for custom flows
78/// - Token issuance requires blockchain interaction
79/// - State transitions must follow protocol rules
80pub struct FungibleTokenMapper<SA, A = CurrentStorage>
81where
82    SA: StorageMapperApi + CallTypeApi,
83    A: StorageAddress<SA>,
84{
85    key: StorageKey<SA>,
86    token_state: TokenMapperState<SA>,
87    address: A,
88}
89
90impl<SA> StorageMapper<SA> for FungibleTokenMapper<SA, CurrentStorage>
91where
92    SA: StorageMapperApi + CallTypeApi,
93{
94    fn new(base_key: StorageKey<SA>) -> Self {
95        Self {
96            token_state: storage_get(base_key.as_ref()),
97            key: base_key,
98            address: CurrentStorage,
99        }
100    }
101}
102
103impl<SA> StorageMapperFromAddress<SA> for FungibleTokenMapper<SA, ManagedAddress<SA>>
104where
105    SA: StorageMapperApi + CallTypeApi,
106{
107    fn new_from_address(address: ManagedAddress<SA>, base_key: StorageKey<SA>) -> Self {
108        Self {
109            token_state: storage_get(base_key.as_ref()),
110            key: base_key,
111            address,
112        }
113    }
114}
115
116impl<SA> FungibleTokenMapper<SA, CurrentStorage>
117where
118    SA: StorageMapperApi + CallTypeApi,
119{
120    /// 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.
121    ///
122    /// #[callback]
123    /// fn my_custom_callback(
124    ///     &self,
125    ///     #[call_result] result: ManagedAsyncCallResult<()>,
126    /// ) {
127    ///      match result {
128    ///     ManagedAsyncCallResult::Ok(token_id) => {
129    ///         self.fungible_token_mapper().set_token_id(token_id);
130    ///     },
131    ///     ManagedAsyncCallResult::Err(_) => {
132    ///         self.fungible_token_mapper().clear();
133    ///     },
134    /// }
135    ///
136    /// If you want to use default callbacks, import the default_issue_callbacks::DefaultIssueCallbacksModule from multiversx-sc-modules
137    /// and pass None for the opt_callback argument
138    pub fn issue(
139        &self,
140        issue_cost: BigUint<SA>,
141        token_display_name: ManagedBuffer<SA>,
142        token_ticker: ManagedBuffer<SA>,
143        initial_supply: BigUint<SA>,
144        num_decimals: usize,
145        opt_callback: Option<CallbackClosure<SA>>,
146    ) -> ! {
147        self.check_not_set();
148
149        let callback = match opt_callback {
150            Some(cb) => cb,
151            None => self.default_callback_closure_obj(&initial_supply),
152        };
153        let properties = FungibleTokenProperties {
154            num_decimals,
155            ..Default::default()
156        };
157
158        storage_set(self.get_storage_key(), &TokenMapperState::<SA>::Pending);
159        Tx::new_tx_from_sc()
160            .to(ESDTSystemSCAddress)
161            .typed(ESDTSystemSCProxy)
162            .issue_fungible(
163                issue_cost,
164                &token_display_name,
165                &token_ticker,
166                &initial_supply,
167                properties,
168            )
169            .callback(callback)
170            .async_call_and_exit()
171    }
172
173    /// 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.
174    ///
175    /// #[callback]
176    /// fn my_custom_callback(
177    ///     &self,
178    ///     #[call_result] result: ManagedAsyncCallResult<()>,
179    /// ) {
180    ///      match result {
181    ///     ManagedAsyncCallResult::Ok(token_id) => {
182    ///         self.fungible_token_mapper().set_token_id(token_id);
183    ///     },
184    ///     ManagedAsyncCallResult::Err(_) => {
185    ///         self.fungible_token_mapper().clear();
186    ///     },
187    /// }
188    ///
189    /// If you want to use default callbacks, import the default_issue_callbacks::DefaultIssueCallbacksModule from multiversx-sc-modules
190    /// and pass None for the opt_callback argument
191    pub fn issue_and_set_all_roles(
192        &self,
193        issue_cost: BigUint<SA>,
194        token_display_name: ManagedBuffer<SA>,
195        token_ticker: ManagedBuffer<SA>,
196        num_decimals: usize,
197        opt_callback: Option<CallbackClosure<SA>>,
198    ) -> ! {
199        self.check_not_set();
200
201        let callback = match opt_callback {
202            Some(cb) => cb,
203            None => self.default_callback_closure_obj(&BigUint::zero()),
204        };
205
206        storage_set(self.get_storage_key(), &TokenMapperState::<SA>::Pending);
207        Tx::new_tx_from_sc()
208            .to(ESDTSystemSCAddress)
209            .typed(ESDTSystemSCProxy)
210            .issue_and_set_all_roles(
211                issue_cost,
212                token_display_name,
213                token_ticker,
214                EsdtTokenType::Fungible,
215                num_decimals,
216            )
217            .callback(callback)
218            .async_call_and_exit();
219    }
220
221    pub fn clear(&mut self) {
222        let state: TokenMapperState<SA> = storage_get(self.key.as_ref());
223        if state.is_pending() {
224            storage_clear(self.key.as_ref());
225        }
226    }
227
228    pub fn mint(&self, amount: BigUint<SA>) -> EsdtTokenPayment<SA> {
229        let send_wrapper = SendWrapper::<SA>::new();
230        let token_id = self.get_token_id();
231
232        send_wrapper.esdt_local_mint(&token_id, 0, &amount);
233
234        EsdtTokenPayment::new(token_id, 0, amount)
235    }
236
237    pub fn mint_and_send(
238        &self,
239        to: &ManagedAddress<SA>,
240        amount: BigUint<SA>,
241    ) -> EsdtTokenPayment<SA> {
242        let payment = self.mint(amount);
243        self.send_payment(to, &payment);
244
245        payment
246    }
247
248    pub fn burn(&self, amount: &BigUint<SA>) {
249        let send_wrapper = SendWrapper::<SA>::new();
250        let token_id = self.get_token_id_ref();
251
252        send_wrapper.esdt_local_burn(token_id, 0, amount);
253    }
254
255    pub fn send_payment(&self, to: &ManagedAddress<SA>, payment: &EsdtTokenPayment<SA>) {
256        Tx::new_tx_from_sc()
257            .to(to)
258            .single_esdt(&payment.token_identifier, 0, &payment.amount)
259            .transfer();
260    }
261
262    pub fn set_if_empty(&mut self, token_id: EsdtTokenIdentifier<SA>) {
263        if self.is_empty() {
264            self.set_token_id(token_id);
265        }
266    }
267
268    pub fn set_local_roles(
269        &self,
270        roles: &[EsdtLocalRole],
271        opt_callback: Option<CallbackClosure<SA>>,
272    ) -> ! {
273        let own_sc_address = Self::get_sc_address();
274        self.set_local_roles_for_address(&own_sc_address, roles, opt_callback);
275    }
276
277    pub fn set_local_roles_for_address(
278        &self,
279        address: &ManagedAddress<SA>,
280        roles: &[EsdtLocalRole],
281        opt_callback: Option<CallbackClosure<SA>>,
282    ) -> ! {
283        self.require_issued_or_set();
284
285        let token_id = self.get_token_id_ref();
286        Tx::new_tx_from_sc()
287            .to(ESDTSystemSCAddress)
288            .typed(ESDTSystemSCProxy)
289            .set_special_roles(address, token_id, roles[..].iter().cloned())
290            .callback(opt_callback)
291            .async_call_and_exit()
292    }
293
294    pub fn set_token_id(&mut self, token_id: EsdtTokenIdentifier<SA>) {
295        self.store_token_id(&token_id);
296        self.token_state = TokenMapperState::Token(token_id);
297    }
298
299    pub(crate) fn store_token_id(&self, token_id: &EsdtTokenIdentifier<SA>) {
300        if self.get_token_state().is_set() {
301            SA::error_api_impl().signal_error(TOKEN_ID_ALREADY_SET_ERR_MSG);
302        }
303        if !token_id.is_valid_esdt_identifier() {
304            SA::error_api_impl().signal_error(INVALID_TOKEN_ID_ERR_MSG);
305        }
306        storage_set(
307            self.get_storage_key(),
308            &TokenMapperState::Token(token_id.clone()),
309        );
310    }
311
312    pub fn get_balance(&self) -> BigUint<SA> {
313        let b_wrapper = BlockchainWrapper::new();
314        let own_sc_address = Self::get_sc_address();
315        let token_id = self.get_token_id_ref();
316
317        b_wrapper.get_esdt_balance(&own_sc_address, token_id, 0)
318    }
319
320    pub fn get_sc_address() -> ManagedAddress<SA> {
321        let b_wrapper = BlockchainWrapper::new();
322        b_wrapper.get_sc_address()
323    }
324}
325
326impl<SA, A> FungibleTokenMapper<SA, A>
327where
328    SA: StorageMapperApi + CallTypeApi,
329    A: StorageAddress<SA>,
330{
331    pub fn get_storage_key(&self) -> ManagedRef<'_, SA, StorageKey<SA>> {
332        self.key.as_ref()
333    }
334
335    pub fn get_token_state(&self) -> TokenMapperState<SA> {
336        self.token_state.clone()
337    }
338
339    pub fn get_token_id(&self) -> EsdtTokenIdentifier<SA> {
340        if let TokenMapperState::Token(token) = &self.token_state {
341            token.clone()
342        } else {
343            SA::error_api_impl().signal_error(INVALID_TOKEN_ID_ERR_MSG)
344        }
345    }
346
347    pub fn get_token_id_ref(&self) -> &EsdtTokenIdentifier<SA> {
348        if let TokenMapperState::Token(token) = &self.token_state {
349            token
350        } else {
351            SA::error_api_impl().signal_error(INVALID_TOKEN_ID_ERR_MSG);
352        }
353    }
354
355    pub fn is_empty(&self) -> bool {
356        storage_get_len(self.get_storage_key()) == 0
357    }
358
359    pub fn require_issued_or_set(&self) {
360        if self.is_empty() {
361            SA::error_api_impl().signal_error(MUST_SET_TOKEN_ID_ERR_MSG);
362        }
363    }
364
365    pub fn require_same_token(&self, expected_token_id: &EsdtTokenIdentifier<SA>) {
366        let actual_token_id = self.get_token_id_ref();
367        if actual_token_id != expected_token_id {
368            SA::error_api_impl().signal_error(INVALID_PAYMENT_TOKEN_ERR_MSG);
369        }
370    }
371
372    pub fn require_all_same_token(&self, payments: &ManagedVec<SA, EsdtTokenPayment<SA>>) {
373        let actual_token_id = self.get_token_id_ref();
374        for p in payments {
375            if actual_token_id != &p.token_identifier {
376                SA::error_api_impl().signal_error(INVALID_PAYMENT_TOKEN_ERR_MSG);
377            }
378        }
379    }
380
381    pub fn default_callback_closure_obj(
382        &self,
383        initial_supply: &BigUint<SA>,
384    ) -> CallbackClosure<SA> {
385        let initial_caller = BlockchainWrapper::<SA>::new().get_caller();
386        let cb_name = if initial_supply > &0 {
387            DEFAULT_ISSUE_WITH_INIT_SUPPLY_CALLBACK_NAME
388        } else {
389            DEFAULT_ISSUE_CALLBACK_NAME
390        };
391
392        let mut cb_closure = CallbackClosure::new(cb_name);
393        cb_closure.push_endpoint_arg(&initial_caller);
394        cb_closure.push_endpoint_arg(&self.key.buffer);
395
396        cb_closure
397    }
398
399    pub(crate) fn check_not_set(&self) {
400        let storage_value: TokenMapperState<SA> = storage_get(self.get_storage_key());
401        match storage_value {
402            TokenMapperState::NotSet => {}
403            TokenMapperState::Pending => {
404                SA::error_api_impl().signal_error(PENDING_ERR_MSG);
405            }
406            TokenMapperState::Token(_) => {
407                SA::error_api_impl().signal_error(TOKEN_ID_ALREADY_SET_ERR_MSG);
408            }
409        }
410    }
411}
412
413impl<SA> TopEncodeMulti for FungibleTokenMapper<SA>
414where
415    SA: StorageMapperApi + CallTypeApi,
416{
417    fn multi_encode_or_handle_err<O, H>(&self, output: &mut O, h: H) -> Result<(), H::HandledErr>
418    where
419        O: TopEncodeMultiOutput,
420        H: EncodeErrorHandler,
421    {
422        if self.is_empty() {
423            output.push_single_value(&ManagedBuffer::<SA>::new(), h)
424        } else {
425            output.push_single_value(&self.get_token_id(), h)
426        }
427    }
428}
429
430impl<SA> TypeAbiFrom<FungibleTokenMapper<SA>> for EsdtTokenIdentifier<SA> where
431    SA: StorageMapperApi + CallTypeApi
432{
433}
434
435impl<SA> TypeAbiFrom<Self> for FungibleTokenMapper<SA> where SA: StorageMapperApi + CallTypeApi {}
436
437impl<SA> TypeAbi for FungibleTokenMapper<SA>
438where
439    SA: StorageMapperApi + CallTypeApi,
440{
441    type Unmanaged = Self;
442
443    fn type_name() -> TypeName {
444        EsdtTokenIdentifier::<SA>::type_name()
445    }
446
447    fn type_name_rust() -> TypeName {
448        EsdtTokenIdentifier::<SA>::type_name_rust()
449    }
450
451    fn provide_type_descriptions<TDC: crate::abi::TypeDescriptionContainer>(accumulator: &mut TDC) {
452        EsdtTokenIdentifier::<SA>::provide_type_descriptions(accumulator);
453    }
454
455    fn is_variadic() -> bool {
456        false
457    }
458}