Skip to main content

ESDT Transfers

Transfer fungible ESDT tokens between addresses.

What is ESDT?​

ESDT (eStandard Digital Token) is MultiversX's native token standard. Unlike EGLD transfers, ESDT transfers use encoded data in the transaction.

Simple ESDT Transfer​

import 'package:abidock_mvx/abidock_mvx.dart';
import 'dart:convert';
import 'dart:typed_data';

void main() async {
final provider = GatewayNetworkProvider.devnet();
final account = await Account.fromMnemonic('your mnemonic...');

// Get account info
final config = await provider.getNetworkConfig();
final accountInfo = await provider.getAccount(account.address);

// Token details
final tokenIdentifier = 'USDC-c76f1f';
final amount = BigInt.from(1000000); // 1 USDC (6 decimals)
final recipient = Address.fromBech32('erd1...recipient...');

// Build ESDT transfer data
final data = 'ESDTTransfer@${_hexEncode(tokenIdentifier)}@${amount.toRadixString(16)}';

// Create transaction
final tx = Transaction(
sender: account.address,
receiver: recipient,
value: Balance.zero(),
nonce: accountInfo.nonce,
gasLimit: GasLimit(500000),
gasPrice: GasPrice(1000000000),
chainId: ChainId(config.chainId),
version: TransactionVersion(1),
data: Uint8List.fromList(utf8.encode(data)),
);

final signature = await account.signTransaction(tx);
final signed = tx.copyWith(newSignature: Signature.fromUint8List(signature));
final hash = await provider.sendTransaction(signed);

print('Sent: $hash');
}

String _hexEncode(String str) {
return str.codeUnits.map((c) => c.toRadixString(16).padLeft(2, '0')).join();
}

Token Decimals​

Different tokens have different decimal places:

TokenDecimals1 Unit
USDC61000000
WEGLD181000000000000000000
MEX181000000000000000000
// Helper function
BigInt tokenAmount(double amount, int decimals) {
return BigInt.from(amount * pow(10, decimals));
}

// Usage
final oneUsdc = tokenAmount(1.0, 6); // 1000000
final oneWegld = tokenAmount(1.0, 18); // 1000000000000000000

Check Token Balance​

void main() async {
final provider = GatewayNetworkProvider.devnet();
final address = Address.fromBech32('erd1...');

// Get all ESDT tokens
final tokens = await provider.getFungibleTokensOfAccount(address);

for (final token in tokens) {
print('${token.identifier}: ${token.balance}');
}

// Get specific token
final usdcBalance = await provider.getTokenOfAccount(
address,
'USDC-c76f1f',
);
print('USDC Balance: ${usdcBalance.balance}');
}

Transfer to Smart Contract​

When sending ESDT to a contract with a function call, use SmartContractController:

void main() async {
final provider = GatewayNetworkProvider.devnet();
final account = await Account.fromMnemonic('your mnemonic...');

final config = await provider.getNetworkConfig();
final networkAccount = await provider.getAccount(account.address);

// Load contract ABI
final abi = SmartContractAbi.fromJson(abiJson);
final contractAddress = Address.fromBech32('erd1qqq...');

final controller = SmartContractController(
contractAddress: contractAddress,
abi: abi,
networkProvider: provider,
);

// Create ESDT transfer + contract call
final tx = await controller.call(
account: account,
nonce: networkAccount.nonce,
endpointName: 'deposit',
arguments: [], // Function arguments if any (native Dart types)
tokenTransfers: [
TokenTransferValue.fromPrimitives(
tokenIdentifier: 'WEGLD-bd4d79',
amount: BigInt.parse('500000000000000000'), // 0.5 WEGLD
),
],
options: BaseControllerInput(gasLimit: GasLimit(15000000)),
);

final hash = await provider.sendTransaction(tx);
print('Deposit: $hash');
}

For simple transfers without contract calls, use TransfersController:

final transfersController = TransfersController(
chainId: ChainId(config.chainId),
);

final tx = await transfersController.createTransactionForTokenTransfer(
account,
networkAccount.nonce,
TokenTransferInput(
receiver: recipient,
transfers: [
TokenTransfer.fungible(
tokenIdentifier: 'WEGLD-bd4d79',
amount: BigInt.parse('500000000000000000'),
),
],
),
);

Complete Example​

import 'package:abidock_mvx/abidock_mvx.dart';
import 'dart:math';

void main() async {
print('=== ESDT Transfer Demo ===\n');

final provider = GatewayNetworkProvider.devnet();
final account = await Account.fromMnemonic('your mnemonic...');
final signer = UserSigner(account.secretKey);

print('Sender: ${account.address.bech32}');

// Get account info
final config = await provider.getNetworkConfig();
final networkAccount = await provider.getAccount(account.address);

// Check token balance
final tokenId = 'WEGLD-bd4d79';

try {
final tokenBalance = await provider.getTokenOfAccount(
account.address,
tokenId,
);
final balanceValue = BigInt.parse(tokenBalance.balance);
print('Balance: ${balanceValue.toDouble() / 1e18} WEGLD');
} on NetworkException catch (e) {
if (e.statusCode == 404) {
print('Token not found or zero balance');
} else {
print('Network error: ${e.message}');
}
return;
}

// Transfer details
final recipient = Address.fromBech32(
'erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx'
);
final amount = BigInt.parse('10000000000000000'); // 0.01 WEGLD

// Build ESDT transfer using TransferTransactionsFactory
final factory = TransferTransactionsFactory(
config: TransferTransactionsConfig(chainId: ChainId(config.chainId)),
);

final tx = factory.createTransactionForEsdtTransfer(
sender: account.address,
receiver: recipient,
tokenTransfers: [
TokenTransfer.fungible(
tokenIdentifier: tokenId,
amount: amount,
),
],
);

print(' Token: $tokenId');
print(' Amount: ${amount.toDouble() / 1e18} WEGLD');
print(' To: ${recipient.bech32.substring(0, 20)}...');

// Set nonce and sign transaction
final txWithNonce = tx.copyWith(newNonce: networkAccount.nonce);
final signature = await signer.sign(txWithNonce.serializeForSigning());
final signed = txWithNonce.copyWith(newSignature: Signature.fromUint8List(signature));

// Send
final hash = await provider.sendTransaction(signed);

// Wait for confirmation
final watcher = TransactionWatcher(networkProvider: provider);
final result = await watcher.awaitCompleted(hash);

}

Gas Estimation​

TransferTransactionsFactory computes the gas limit for you. It sums two terms:

gasLimit = (minGasLimit + gasLimitPerByte * data.length) + executionGas

with minGasLimit = 50,000 and gasLimitPerByte = 1,500 by default. The execution term depends on the transfer protocol used:

OperationExecution gasTotal
Single fungible ESDT (ESDTTransfer)300,000350,000 + 1,500 x data bytes
Single NFT/SFT (ESDTNFTTransfer)1,000,0001,050,000 + 1,500 x data bytes
Multi-transfer of N tokens (MultiESDTNFTTransfer)800,000 + 200,000 x N850,000 + 200,000 x N + 1,500 x data bytes

Note that createTransactionForEsdtTransfer switches protocol on list length: one entry uses the single-transfer path, two or more use MultiESDTNFTTransfer.

Pass an explicit gasLimit to override the calculation entirely:

final transfersFactory = TransferTransactionsFactory(
config: TransferTransactionsConfig(chainId: ChainId(config.chainId)),
);

final overriddenTx = transfersFactory.createTransactionForEsdtTransfer(
sender: account.address,
receiver: recipient,
tokenTransfers: [
TokenTransfer.fungible(
tokenIdentifier: 'WEGLD-bd4d79',
amount: BigInt.parse('10000000000000000'),
),
],
gasLimit: GasLimit(600000), // skips the automatic calculation
);

When you attach tokens to a contract call, the endpoint's own execution cost is on top of the transfer cost, so supply it through BaseControllerInput(gasLimit: ...) -- SmartContractController.call requires it.

Next Steps​