Skip to main content

Token Management

Create, issue, and manage ESDT tokens including fungible, semi-fungible, non-fungible, and Meta-ESDT tokens.

Overview

The TokenManagementController provides a complete API for ESDT token lifecycle management:

  • Issue tokens - Create new fungible, NFT, SFT, or Meta-ESDT collections
  • Manage roles - Assign minting, burning, and transfer roles
  • NFT operations - Create, mint, burn NFTs with attributes
  • Token control - Pause, freeze, wipe, and change ownership

TokenManagementController

import 'package:abidock_mvx/abidock_mvx.dart';

final controller = TokenManagementController(
chainId: const ChainId.devnet(),
gasLimitEstimator: GasEstimator(networkProvider: provider),
);

Issuing Tokens

Fungible Token

Create a standard fungible ESDT token:

import 'dart:io';

// Account.fromPem takes the PEM *content*, not a file path.
final pemContent = await File('wallet.pem').readAsString();
final account = await Account.fromPem(pemContent);
final accountInfo = await provider.getAccount(account.address);

final input = IssueFungibleInput(
tokenName: 'MyToken',
tokenTicker: 'MTK',
initialSupply: BigInt.from(1000000000000000000), // 1 billion
numDecimals: BigInt.from(18),
canFreeze: true,
canWipe: true,
canPause: true,
canChangeOwner: true,
canUpgrade: true,
canAddSpecialRoles: true,
);

final tx = await controller.createTransactionForIssuingFungible(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

final hash = await provider.sendTransaction(tx);
print('Issue transaction: $hash');

Non-Fungible Token (NFT)

Create an NFT collection:

final input = IssueNonFungibleInput(
tokenName: 'MyNFTCollection',
tokenTicker: 'MNFT',
canFreeze: true,
canWipe: true,
canPause: false,
canChangeOwner: true,
canUpgrade: true,
canAddSpecialRoles: true,
canTransferNFTCreateRole: true,
);

final tx = await controller.createTransactionForIssuingNonFungible(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Semi-Fungible Token (SFT)

Create an SFT collection (NFTs with quantity):

final input = IssueSemiFungibleInput(
tokenName: 'MySFTCollection',
tokenTicker: 'MSFT',
canFreeze: true,
canWipe: true,
canPause: false,
canChangeOwner: true,
canUpgrade: true,
canAddSpecialRoles: true,
canTransferNFTCreateRole: true,
);

final tx = await controller.createTransactionForIssuingSemiFungible(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Meta-ESDT

Create a Meta-ESDT token (fungible with NFT properties):

final input = RegisterMetaESDTInput(
tokenName: 'MyMetaToken',
tokenTicker: 'MMETA',
numDecimals: BigInt.from(18),
canFreeze: true,
canWipe: true,
canPause: true,
canChangeOwner: true,
canUpgrade: true,
canAddSpecialRoles: true,
canTransferNFTCreateRole: true,
);

final tx = await controller.createTransactionForRegisteringMetaEsdt(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Managing Roles

Set Fungible Token Roles

final input = FungibleSpecialRoleInput(
tokenIdentifier: 'MTK-abc123',
user: Address.fromBech32('erd1...'),
addRoleLocalMint: true,
addRoleLocalBurn: true,
addRoleESDTTransferRole: false,
);

final tx = await controller.createTransactionForSettingSpecialRoleOnFungibleToken(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Set NFT/SFT Roles

final input = SpecialRoleInput(
tokenIdentifier: 'MNFT-abc123',
user: Address.fromBech32('erd1...'),
addRoleNFTCreate: true,
addRoleNFTBurn: true,
addRoleNFTUpdateAttributes: true,
addRoleNFTAddURI: true,
addRoleESDTTransferRole: false,
);

final tx = await controller.createTransactionForSettingSpecialRoleOnNonFungibleToken(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Unset Roles

final input = UnsetFungibleSpecialRoleInput(
tokenIdentifier: 'MTK-abc123',
user: Address.fromBech32('erd1...'),
removeRoleLocalMint: true,
removeRoleLocalBurn: false,
removeRoleESDTTransferRole: false,
);

final tx = await controller.createTransactionForUnsettingSpecialRoleOnFungibleToken(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Set SFT/Meta-ESDT Roles

final input = SemiFungibleSpecialRoleInput(
tokenIdentifier: 'MSFT-abc123',
user: Address.fromBech32('erd1...'),
addRoleNFTCreate: true,
addRoleNFTBurn: true,
addRoleNFTAddQuantity: true,
addRoleESDTTransferRole: false,
);

// For SFT
final sftTx = await controller.createTransactionForSettingSpecialRoleOnSemiFungibleToken(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

// For Meta-ESDT (same input type)
final metaTx = await controller.createTransactionForSettingSpecialRoleOnMetaESDT(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

NFT Operations

Create NFT

final input = MintInput(
tokenIdentifier: 'MNFT-abc123',
initialQuantity: BigInt.one,
name: 'My First NFT',
royalties: 500, // 5% (max 10000 = 100%)
hash: '', // Optional content hash
attributes: Uint8List.fromList(utf8.encode('metadata:ipfs://...')),
uris: ['https://example.com/nft/1.json'],
);

final tx = await controller.createTransactionForCreatingNft(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Add NFT Quantity (SFT)

final input = UpdateQuantityInput(
tokenIdentifier: 'MSFT-abc123',
tokenNonce: BigInt.from(1),
quantity: BigInt.from(100),
);

final tx = await controller.createTransactionForAddingQuantity(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Burn NFT Quantity

final input = UpdateQuantityInput(
tokenIdentifier: 'MSFT-abc123',
tokenNonce: BigInt.from(1),
quantity: BigInt.from(50),
);

final tx = await controller.createTransactionForBurningQuantity(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Token Control

Pause/Unpause Token

// Pause token transfers
final pauseInput = PausingInput(tokenIdentifier: 'MTK-abc123');
final pauseTx = await controller.createTransactionForPausing(
account,
accountInfo.nonce,
pauseInput,
const BaseControllerInput(),
);

// Unpause token transfers
final unpauseInput = PausingInput(tokenIdentifier: 'MTK-abc123');
final unpauseTx = await controller.createTransactionForUnpausing(
account,
accountInfo.nonce,
unpauseInput,
const BaseControllerInput(),
);

Freeze/Unfreeze Account

// Freeze specific account
final freezeInput = ManagementInput(
tokenIdentifier: 'MTK-abc123',
user: Address.fromBech32('erd1...'),
);
final freezeTx = await controller.createTransactionForFreezing(
account,
accountInfo.nonce,
freezeInput,
const BaseControllerInput(),
);

// Unfreeze account
final unfreezeInput = ManagementInput(
tokenIdentifier: 'MTK-abc123',
user: Address.fromBech32('erd1...'),
);
final unfreezeTx = await controller.createTransactionForUnfreezing(
account,
accountInfo.nonce,
unfreezeInput,
const BaseControllerInput(),
);

Wipe Frozen Account

final input = ManagementInput(
tokenIdentifier: 'MTK-abc123',
user: Address.fromBech32('erd1...'),
);

final tx = await controller.createTransactionForWiping(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Minting & Burning

Local Mint

final input = LocalMintInput(
tokenIdentifier: 'MTK-abc123',
supplyToMint: BigInt.from(1000000000000000000), // 1 token
);

final tx = await controller.createTransactionForLocalMinting(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Local Burn

final input = LocalBurnInput(
tokenIdentifier: 'MTK-abc123',
supplyToBurn: BigInt.from(1000000000000000000), // 1 token
);

final tx = await controller.createTransactionForLocalBurning(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Advanced Token Operations

Modify Royalties

final input = ModifyRoyaltiesInput(
tokenIdentifier: 'MNFT-abc123',
tokenNonce: BigInt.from(1),
newRoyalties: BigInt.from(750), // 7.5%
);

final tx = await controller.createTransactionForModifyingRoyalties(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Set New URIs

final input = SetNewUriInput(
tokenIdentifier: 'MNFT-abc123',
tokenNonce: BigInt.from(1),
newUris: ['https://new-uri.com/metadata.json'],
);

final tx = await controller.createTransactionForSettingNewUris(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Modify Creator

final input = ModifyCreatorInput(
tokenIdentifier: 'MNFT-abc123',
tokenNonce: BigInt.from(1),
);

final tx = await controller.createTransactionForModifyingCreator(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Update Metadata

final input = ManageMetadataInput(
tokenIdentifier: 'MNFT-abc123',
tokenNonce: BigInt.from(1),
newTokenName: 'Updated NFT Name',
newRoyalties: BigInt.from(500),
newAttributes: Uint8List.fromList(utf8.encode('new:attributes')),
newUris: ['https://example.com/new-metadata.json'],
);

final tx = await controller.createTransactionForUpdatingMetadata(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Change Token to Dynamic

final input = ChangeTokenToDynamicInput(tokenIdentifier: 'MNFT-abc123');

final tx = await controller.createTransactionForChangingTokenToDynamic(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Global Burn Role

final input = BurnRoleGloballyInput(tokenIdentifier: 'MTK-abc123');

// Set global burn role
final setTx = await controller.createTransactionForSettingBurnRoleGlobally(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

// Unset global burn role
final unsetTx = await controller.createTransactionForUnsettingBurnRoleGlobally(
account,
accountInfo.nonce,
input,
const BaseControllerInput(),
);

Parsing Outcomes

Use TokenManagementOutcomeParser to extract results from completed transactions:

final parser = TokenManagementOutcomeParser();

// After transaction is processed
final tx = await provider.getTransaction(hash);

// Parse issue result
final issueResult = parser.parseIssueFungible(tx);
print('Token identifier: ${issueResult.first.tokenIdentifier}');

// Parse NFT creation
final nftResult = parser.parseNftCreate(tx);
print('NFT nonce: ${nftResult.first.nonce}');

// Parse role assignment
final roleResult = parser.parseSetSpecialRole(tx);
print('Assigned roles: ${roleResult.first.roles}');

Available Parsers

MethodResult Type
parseIssueFungibleIssueFungibleResult
parseIssueNonFungibleIssueNonFungibleResult
parseIssueSemiFungibleIssueSemiFungibleResult
parseRegisterMetaEsdtRegisterMetaEsdtResult
parseRegisterAndSetAllRolesRegisterAndSetAllRolesResult
parseSetSpecialRoleSetSpecialRoleResult
parseSetBurnRoleGloballyvoid
parseUnsetBurnRoleGloballyvoid
parseNftCreateNftCreateResult
parseLocalMintLocalMintResult
parseLocalBurnLocalBurnResult
parsePausePauseResult
parseUnpauseUnpauseResult
parseFreezeFreezeResult
parseUnfreezeUnfreezeResult
parseWipeWipeResult
parseUpdateAttributesUpdateAttributesResult
parseAddQuantityAddQuantityResult
parseBurnQuantityBurnQuantityResult
parseModifyRoyaltiesModifyRoyaltiesResult
parseSetNewUrisSetNewUrisResult
parseModifyCreatorModifyCreatorResult
parseUpdateMetadataUpdateMetadataResult
parseMetadataRecreateMetadataRecreateResult
parseChangeTokenToDynamicChangeToDynamicResult
parseRegisterDynamicTokenRegisterDynamicResult
parseRegisterDynamicTokenAndSettingRolesRegisterDynamicResult

Token Properties Reference

PropertyDescriptionOn fungible issue?
canFreezeOwner can freeze accountsYes
canWipeOwner can wipe frozen accountsYes
canPauseOwner can pause all transfersYes
canChangeOwnerOwnership can be transferredYes
canUpgradeProperties can be modifiedYes
canAddSpecialRolesRoles can be assigned to accountsYes
canTransferNFTCreateRoleNFT create role can be transferredNo -- collection endpoints only

How properties reach the system contract

Two behaviours are worth knowing before you reason about a token's final state:

Every pair is emitted, including the false ones. The factory always sends the complete name/value argument list, so canFreeze set to false travels as the explicit pair canFreeze / false. Omitting a pair does not mean "disabled": the ESDT system contract creates a token with canUpgrade and canAddSpecialRoles already enabled and only overrides the properties actually present in the argument list, so a missing pair silently keeps the contract's own default. Emitting every pair is also what lets a controlChanges call switch a property back off -- there is no other way to express that.

Toggling properties after issuance is a factory-level operation. Pass the complete desired end state, not just the flags you want to turn on:

final factory = TokenManagementTransactionsFactory(
config: const TokenManagementConfig(chainId: ChainId.devnet()),
);

final tx = factory.createTransactionForControllingProperties(
sender: account.address,
tokenIdentifier: 'MTK-abc123',
properties: const TokenProperties(
canFreeze: true,
canWipe: false, // written as an explicit `false` pair, so it turns off
canPause: true,
canChangeOwner: true,
canUpgrade: true,
canAddSpecialRoles: true,
),
);

Note that TokenProperties.canUpgrade defaults to true while every other flag defaults to false, matching the system contract's own default for a freshly issued token.

canTransferNFTCreateRole is absent from the fungible issue argument list. It belongs to the collection endpoints (issueNonFungible, issueSemiFungible, registerMetaESDT), which is why IssueFungibleInput has no such field while IssueNonFungibleInput does.

Role Reference

Fungible Token Roles

RoleDescription
ESDTRoleLocalMintMint new tokens
ESDTRoleLocalBurnBurn tokens
ESDTTransferRoleTransfer when restricted

NFT/SFT Roles

RoleDescription
ESDTRoleNFTCreateCreate new NFTs
ESDTRoleNFTBurnBurn NFTs
ESDTRoleNFTUpdateAttributesModify NFT attributes
ESDTRoleNFTAddURIAdd URIs to NFTs
ESDTRoleNFTAddQuantityAdd quantity to SFTs
ESDTTransferRoleTransfer when restricted

Best Practices

  1. Always check properties - Set canAddSpecialRoles: true if you need to assign roles later
  2. Use appropriate decimals - Standard is 18 for fungible tokens
  3. Store token identifiers - Parse and save the identifier from issue transactions
  4. Wait for finalization - Token operations require finalization before roles work
  5. Test on devnet first - Every issue/register call carries a 0.05 EGLD payment to the ESDT system contract, on every network