Skip to main content

Special Types

Special types handle domain-specific MultiversX data: addresses, tokens, hashes, contract metadata and fixed-point decimals.

Wire format

TypeTop-levelNested
Addressexactly 32 bytesexactly 32 bytes
TokenIdentifier, TokenIdUTF-8 bytes, no prefix[u32 length][UTF-8]
EgldOrEsdtTokenIdentifierUTF-8 bytes; native EGLD is a zero-length payload[u32 length][UTF-8]
H256exactly 32 bytesexactly 32 bytes
ManagedByteArray<N>exactly N bytesexactly N bytes, no prefix
CodeMetadataexactly 2 bytes, big-endianexactly 2 bytes, big-endian
ManagedDecimal<N>magnitude only (scale lives in the type)[u32 length][magnitude]
ManagedDecimal<usize>[u32 length][magnitude][u32 scale][u32 length][magnitude][u32 scale]
Nothingemptyempty (consumes 0 bytes)

Address

32-byte MultiversX addresses:

// From bech32 string directly
final address = AddressType.create(
'erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu'
);

// nativeValue returns the bech32 string
final bech32 = address.nativeValue; // String: 'erd1qqq...'
print(bech32);

// Bytes and hex are available on the value itself
print(address.toBech32()); // erd1qqq...
print(address.toHex()); // 0000...

Creating Address Values

// Method 1: Static factory - AddressType.create()
final addr1 = AddressType.create(
'erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu'
);

// Method 2: Direct constructor - AddressValue.fromBech32()
final addr2 = AddressValue.fromBech32(
'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'
);

// Method 3: Via type instance
final addr3 = AddressType.type.createValue(
'erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu'
);

// From hex string
final fromHex = AddressType.create(
'0000000000000000000000000000000000000000000000000000000000000000'
);

// From bytes
final fromBytes = AddressType.create(List<int>.filled(32, 0));

// Zero address (32 zero bytes, used as the deploy receiver)
final zeroAddr = AddressType.createZero();

AddressType.create treats a string starting with erd1 as bech32 and anything else as hex.

TokenIdentifier

ESDT token identifiers:

// Standard ESDT
final token = TokenIdentifierType.create('USDC-123456');
print(token.nativeValue); // 'USDC-123456'

// NFT collection
final nft = TokenIdentifierType.create('MYNFT-abc123');

TokenId in an ABI is the same type with the same wire form.

Token Identifier Format

TICKER-randomhex
└─────┘ └───────┘
3-10 6 hex
chars chars

The value class validates this shape: an upper-case ticker of 3-10 alphanumerics starting with a letter, a dash, exactly 6 lower-case hex characters, and -- for NFT/SFT/MetaESDT instances -- an optional -<hexnonce> suffix. Anything else throws ArgumentError.

Examples:

  • WEGLD-bd4d79 - Wrapped EGLD
  • USDC-c76f1f - USD Coin
  • MEX-455c57 - MEX token
  • MYNFT-abc123-0a - a single NFT of the MYNFT-abc123 collection

EgldOrEsdtTokenIdentifier

Either native EGLD or an ESDT token -- the type payable endpoints use:

// EGLD
final egld = EgldOrEsdtTokenIdentifierType.create('EGLD');
print(egld.nativeValue); // 'EGLD'
print(egld.isEgld); // true

// ESDT
final esdt = EgldOrEsdtTokenIdentifierType.create('USDC-123456');
print(esdt.nativeValue); // 'USDC-123456'
print(esdt.isEsdt); // true

Native EGLD has two accepted spellings: the canonical EGLD-000000, which encodes as a zero-length payload, and the legacy EGLD sentinel, which encodes as its 4 ASCII bytes. Decoding always canonicalises -- an empty payload and EGLD both come back as EGLD-000000.

Token payment structs

EsdtTokenPayment, EgldOrEsdtTokenPayment, EgldOrMultiEsdtPayment, Payment and FungiblePayment are built into the framework: contracts use them without ever listing them in the ABI's types map, and the SDK recognises the names intrinsically.

StructFields, in wire order
EsdtTokenPaymenttoken_identifier: TokenIdentifier, token_nonce: u64, amount: BigUint
EgldOrEsdtTokenPaymenttoken_identifier: EgldOrEsdtTokenIdentifier, token_nonce: u64, amount: BigUint
Paymenttoken_identifier: TokenId, token_nonce: u64, amount: NonZeroBigUint
FungiblePaymenttoken_identifier: TokenId, amount: NonZeroBigUint
EgldOrMultiEsdtPaymentegld_amount: BigUint, multi_esdt: List<EsdtTokenPayment>

TokenId shares the wire form of TokenIdentifier and NonZeroBigUint that of BigUint, so a Payment is [u32 len][utf8 id][8-byte BE nonce][u32 len][magnitude] when nested, and a FungiblePayment is [u32 len][utf8 id][u32 len][magnitude] in both positions.

Define an equivalent struct yourself when you want to build one by hand:

// Define EsdtTokenPayment type
final paymentType = StructBuilder('EsdtTokenPayment')
.field('token_identifier', TokenIdentifierType.type)
.field('token_nonce', U64Type.type)
.field('amount', BigUIntType.type)
.build();

// Create payment value
final payment = paymentType.createValue(<String, dynamic>{
'token_identifier': 'USDC-123456',
'token_nonce': BigInt.zero, // 0 for fungible
'amount': BigInt.from(1000000),
}) as StructValue;

NFT Payment

// NFT with nonce
final nftPayment = paymentType.createValue(<String, dynamic>{
'token_identifier': 'MYNFT-abc123',
'token_nonce': BigInt.from(42), // NFT nonce
'amount': BigInt.one,
}) as StructValue;

For sending tokens with a contract call, use TokenTransferValue.fromPrimitives(...) and the tokenTransfers parameter instead -- the controller builds the ESDTTransfer / MultiESDTNFTTransfer data field for you.

H256

32-byte hash values (256 bits):

// From bytes (must be exactly 32 bytes)
final hash = H256Type.create(List<int>.filled(32, 0));
print(hash.nativeValue); // Uint8List of 32 bytes

// From hex string (64 characters)
final hashHex = H256Type.create(
'0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20'
);

// Common use: transaction hashes, merkle roots

ManagedByteArray

A fixed-size byte buffer whose length is part of the type. arrayN<u8> in an ABI parses to ManagedByteArray<N>:

final keyType = ManagedByteArrayType(32);
final key = ManagedByteArrayValue(keyType, Uint8List(32));
print(key.value.length); // 32

It is written as exactly N raw bytes in both positions -- no length prefix anywhere, because the length is already known from the type.

CodeMetadata

Smart contract deployment metadata, a 16-bit value stored as 2 big-endian bytes:

// From integer flags
final metadata = CodeMetadataType.create(0x0506); // upgradeable + readable + payable + payableBySC

// Check individual flags on the value
print(metadata.isUpgradeable); // true
print(metadata.isReadable); // true
print(metadata.isPayable); // true
print(metadata.isPayableBySC); // true

// From byte array (2 bytes, big-endian)
final fromBytes = CodeMetadataType.create(<int>[0x05, 0x06]);

Metadata Flags (Big-Endian Layout)

FlagHex valueByteBitDescription
upgradeable0x010000Contract can be upgraded
readable0x040002Other contracts can read storage
payable0x000211Can receive EGLD
payableBySC0x000412Can receive from smart contracts

Common Flag Combinations

// Upgradeable only
final upgradeOnly = CodeMetadataType.create(0x0100);

// Upgradeable + readable
final upgradeRead = CodeMetadataType.create(0x0500);

// Upgradeable + payable
final upgradePay = CodeMetadataType.create(0x0102);

// Upgradeable + readable + payable
final standard = CodeMetadataType.create(0x0502);

// All flags (upgradeable + readable + payable + payableBySC)
final allFlags = CodeMetadataType.create(0x0506);

0x0506 is also the default metadata used by createTransactionForDeploy.

ManagedDecimal

Fixed-point decimals. The raw value is an integer; the scale says where the decimal point sits.

// ManagedDecimalType.create(scale, value)
final decimal = ManagedDecimalType.create(
18, // scale: 18 decimal places
BigInt.parse('1500000000000000000'), // raw value = 1.5 * 10^18
);

print(decimal.toDecimalString()); // '1.500000000000000000'
print(decimal.nativeValue); // BigInt 1500000000000000000 (raw)

Scale comes first. The type carries the scale, the value carries the digits:

final priceType = ManagedDecimalType.of(2);             // ManagedDecimal<2>
final price = priceType.createValue(19.99) as ManagedDecimalValue; // from double
final same = priceType.createValue('19.99') as ManagedDecimalValue; // from string
final raw = priceType.createValue(BigInt.from(1999)) as ManagedDecimalValue; // raw digits

// Signed and runtime-scaled variants
final signedType = ManagedDecimalType.signed(4); // ManagedDecimalSigned<4>
final variableType = ManagedDecimalType.variable(2); // ManagedDecimal<usize>

Wire rules worth remembering:

  • Fixed scale (ManagedDecimal<N>): only the magnitude travels; N is never written. Nested, it is still length-prefixed ([u32 length][magnitude]) so the following field can be found.
  • Variable scale (ManagedDecimal<usize>): the scale follows the magnitude as a 4-byte big-endian integer, in both positions.
  • A variadic<ManagedDecimal> with more than one item is rejected, because the items' boundaries would be ambiguous.

Nothing

The unit/void type:

final nothing = NothingType.create();
print(nothing.nativeValue); // null

Used for endpoints that return no value; it encodes to nothing and consumes no bytes.

Complete Example

import 'dart:typed_data';

import 'package:abidock_mvx/abidock_mvx.dart';

void main() {
print('=== Special Types Demo ===\n');

// === Address ===
final address = AddressType.create(
'erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu'
);
print(' Bech32: ${address.nativeValue}');
print(' Hex: ${address.toHex().substring(0, 16)}...');

// === TokenIdentifier ===
final token = TokenIdentifierType.create('WEGLD-bd4d79');
print(' Token: ${token.nativeValue}');

// === EgldOrEsdtTokenIdentifier ===
final egld = EgldOrEsdtTokenIdentifierType.create('EGLD');
final esdt = EgldOrEsdtTokenIdentifierType.create('MEX-455c57');
print(' EGLD: ${egld.nativeValue} (isEgld: ${egld.isEgld})');
print(' ESDT: ${esdt.nativeValue}');

// === EsdtTokenPayment ===
final paymentType = StructBuilder('EsdtTokenPayment')
.field('token_identifier', TokenIdentifierType.type)
.field('token_nonce', U64Type.type)
.field('amount', BigUIntType.type)
.build();

final payment = paymentType.createValue(<String, dynamic>{
'token_identifier': 'USDC-c76f1f',
'token_nonce': BigInt.zero,
'amount': BigInt.from(1000000),
}) as StructValue;
print(' Payment: ${payment.nativeValue}');

// === H256 ===
final hash = H256Type.create(
Uint8List.fromList(List<int>.generate(32, (int i) => i * 8))
);
print(' First 8 bytes: ${hash.nativeValue.take(8).toList()}');
print(' Length: ${hash.nativeValue.length} bytes');

// === CodeMetadata ===
final metadata = CodeMetadataType.create(0x0502);
print(' Upgradeable: ${metadata.isUpgradeable}');
print(' Payable: ${metadata.isPayable}');
print(' Metadata: ${metadata.nativeValue}');

// === ManagedDecimal ===
final price = ManagedDecimalType.create(2, 19.99);
print(' Price: ${price.toDecimalString()}');

// === Nothing ===
final nothing = NothingType.create();
print(' Value: ${nothing.nativeValue}'); // null
}

Common Patterns

Token Amount Formatting

// Format token amount with decimals
String formatAmount(BigInt amount, int decimals) {
final BigInt divisor = BigInt.from(10).pow(decimals);
final BigInt whole = amount ~/ divisor;
final String fraction = (amount % divisor).toString().padLeft(decimals, '0');
return '$whole.$fraction';
}

// Usage
final amount = BigInt.parse('1500000000000000000'); // 1.5 EGLD
print(formatAmount(amount, 18)); // '1.500000000000000000'

Balance.toDenominated / toDenominatedTrimmed do the same for EGLD amounts, and ManagedDecimalValue.toDecimalString() for decimals that carry their own scale.

Working with Token Payments

// Parse an EsdtTokenPayment-shaped struct from a query result
void parsePayment(StructValue payment) {
final tokenId =
payment.getFieldValue('token_identifier').nativeValue as String;
final nonce = payment.getFieldValue('token_nonce').nativeValue as BigInt;
final amount = payment.getFieldValue('amount').nativeValue as BigInt;

if (nonce == BigInt.zero) {
print('Fungible: $tokenId, Amount: $amount');
} else {
print('NFT: $tokenId-${nonce.toRadixString(16)}, Amount: $amount');
}
}

Next Steps