Skip to main content

Composite Types

Composite types combine multiple values into structured data.

Field Type Parameters

Field values use native types based on the field type:

  • U32Type.type fields take int
  • U64Type.type fields take int or BigInt
  • BigUIntType.type fields take int or BigInt
  • StringType.type fields take String
Casting

createValue returns the base TypedValue; cast to StructValue, TupleValue, EnumValue or ExplicitEnumValue to reach their APIs.

Wire format

TypeTop-levelNested
structfields concatenated, each nested-encoded, in declaration orderidentical
tupleelements concatenated, each nested-encodedidentical
enumunit variant with discriminant 0 is an empty buffer; otherwise [u8 discriminant] + nested fieldsalways [u8 discriminant] + nested fields
explicit-enumUTF-8 bytes of the variant name[u32 length][UTF-8 variant name]

Struct and tuple encodings carry no field names, no separators and no length prefix: field order in the ABI is the format. Discriminants must fit in a byte (0-255); anything else throws AbiBinaryCodecException.

Struct

Named fields with different types:

Creating Struct Types

// Define a struct type using StructBuilder
final userType = StructBuilder('User')
.field('name', StringType.type)
.field('age', U32Type.type) // u32 takes int
.field('balance', BigUIntType.type) // BigUint takes int or BigInt
.build();

// Create a value from native Dart types
final user = userType.createValue(<String, dynamic>{
'name': 'Alice',
'age': 30, // int for u32
'balance': BigInt.from(1000), // BigInt for BigUint
}) as StructValue;

// Access as map
print(user.nativeValue); // {name: Alice, age: 30, balance: 1000}

createValue also accepts a List with the values in field order, which is handy when the source data is positional.

Accessing Fields

// Get a field value (throws ArgumentError if the field does not exist)
final name = user.getFieldValue('name');
print(name.nativeValue); // 'Alice'

final balance = user.getFieldValue('balance');
print(balance.nativeValue); // BigInt 1000

// Ask without throwing
if (user.tryGetFieldValue('email') == null) {
print('No email field');
}

// Field names and values in declaration order
print(user.fieldNames); // [name, age, balance]
print(user.fieldValues.length); // 3

Nested Structs

// Define Address type
final addressType = StructBuilder('Address')
.field('street', StringType.type)
.field('city', StringType.type)
.build();

// Define User with nested Address type
final userType = StructBuilder('UserWithAddress')
.field('name', StringType.type)
.field('address', addressType)
.build();

// Create user with nested address using native values
final userWithAddress = userType.createValue(<String, dynamic>{
'name': 'Alice',
'address': <String, dynamic>{
'street': '123 Main St',
'city': 'New York',
},
}) as StructValue;

// Access nested data
final addrStruct = userWithAddress.getFieldValue('address') as StructValue;
final city = addrStruct.getFieldValue('city').nativeValue;
print(city); // 'New York'

Enum

Variants with optional data:

Simple Enum (No Fields)

// Define enum type with simple variants
final statusType = EnumBuilder('Status')
.variant('Pending', 0)
.variant('Active', 1)
.variant('Completed', 2)
.build();

// Create enum values
final pending = statusType.createValue('Pending') as EnumValue;
print(pending.nativeValue); // 'Pending'
print(pending.discriminant); // 0

final active = statusType.createValue('Active') as EnumValue;
print(active.nativeValue); // 'Active'
print(active.discriminant); // 1

Pending encodes as an empty buffer at the top level (discriminant 0, no fields) and as 0x00 when nested; Active is 0x01 in both positions.

Enum with Fields

// Define enum type where variants carry data
final resultType = EnumBuilder('Result')
.variantWithFields('Success', 0, <AbiType>[U64Type.type])
.variantWithFields('Error', 1, <AbiType>[StringType.type])
.build();

// Create a Success value
final success = resultType.createValue(<String, dynamic>{
'variant': 'Success',
'fields': <dynamic>[BigInt.from(42)],
}) as EnumValue;

// Access variant and fields
final data = success.nativeValue as Map<String, dynamic>;
print(data['variant']); // 'Success'
print(data['fields']); // [42]

A variant that carries fields always writes its discriminant, even when it is 0 -- otherwise the fields would have nothing to hang off.

Complex Enum Example

// A Result-shaped enum
final resultType = EnumBuilder('Result')
.variantWithFields('Ok', 0, <AbiType>[StringType.type])
.variantWithFields('Err', 1, <AbiType>[StringType.type])
.build();

// Create Ok result
final okResult = resultType.createValue(<String, dynamic>{
'variant': 'Ok',
'fields': <dynamic>['Success!'],
}) as EnumValue;

// Create Error result
final errResult = resultType.createValue(<String, dynamic>{
'variant': 'Err',
'fields': <dynamic>['Something went wrong'],
}) as EnumValue;

// Handling either shape
void handleResult(EnumValue result) {
final dynamic data = result.nativeValue;
if (data is String) {
// Unit variant (no fields)
print('Simple: $data');
} else if (data is Map) {
final dynamic variant = data['variant'];
final List<dynamic> fields = data['fields'] as List<dynamic>;

if (variant == 'Ok') {
print('Success: ${fields[0]}');
} else if (variant == 'Err') {
print('Error: ${fields[0]}');
}
}
}

Explicit Enum

An explicit enum has named variants and never carries data. The decisive difference is the wire format: it travels as the variant name, not as a discriminant byte, which is what lets a contract evolve the variant order without breaking clients.

Defining Explicit Enums

// Define an explicit enum type
final statusType = ExplicitEnumType(
name: 'Status',
variants: <ExplicitEnumVariantDefinition>[
ExplicitEnumVariantDefinition(name: 'Pending', discriminant: 0),
ExplicitEnumVariantDefinition(name: 'Active', discriminant: 1),
ExplicitEnumVariantDefinition(name: 'Completed', discriminant: 2),
ExplicitEnumVariantDefinition(name: 'Cancelled', discriminant: 3),
],
);

Creating Explicit Enum Values

// Create by discriminant (the number declared in the ABI, not a position)
final pending = statusType.createValue(0) as ExplicitEnumValue; // Status::Pending
final active = statusType.createValue(1) as ExplicitEnumValue; // Status::Active

// Create by variant name (string)
final completed = statusType.createValue('Completed') as ExplicitEnumValue;

Accessing Values

final enumValue = statusType.createValue(1) as ExplicitEnumValue;

print(enumValue.discriminant); // 1
print(enumValue.variantName); // 'Active'
print(enumValue.nativeValue); // 'Active' (variant name as String)

The discriminant is local bookkeeping: it never reaches the wire. Decoding an unknown variant name throws AbiBinaryCodecException.

Explicit Enum vs Regular Enum

FeatureExplicit EnumRegular Enum
FieldsNoneOptional data fields per variant
Wire formUTF-8 variant name (length-prefixed when nested)1-byte discriminant + nested fields
Order sensitivityNames are the contractDiscriminants are the contract
ABI type kindexplicit-enumenum
// Explicit enum: simple variant names only
final statusType = ExplicitEnumType(
name: 'Status',
variants: <ExplicitEnumVariantDefinition>[
ExplicitEnumVariantDefinition(name: 'Pending', discriminant: 0),
ExplicitEnumVariantDefinition(name: 'Active', discriminant: 1),
],
);

// Regular enum: variants can carry data (field types, in order)
final resultType = EnumType(
name: 'Result',
variants: <EnumVariantDefinition>[
EnumVariantDefinition(
name: 'Ok',
discriminant: 0,
fields: <AbiType>[BigUIntType.type],
),
EnumVariantDefinition(
name: 'Err',
discriminant: 1,
fields: <AbiType>[StringType.type],
),
],
);

Tuple

Ordered collection of different types (positional access):

Creating Tuples

// Define tuple type with (u64, bool, utf-8 string) elements
final tupleType = TupleType(<AbiType>[
U64Type.type,
BooleanType.type,
StringType.type,
]);

// Create tuple value using native values
final tuple = tupleType.createValue(<dynamic>[
BigInt.from(123),
true,
'hello',
]) as TupleValue;

// Access as list
print(tuple.nativeValue); // [123, true, hello]

Accessing Tuple Elements

final first = tuple.elements[0].nativeValue;  // BigInt 123
final second = tuple.elements[1].nativeValue; // true
final third = tuple.elements[2].nativeValue; // 'hello'

// Index operator does the same
final alsoFirst = tuple[0].nativeValue;

Tuple vs Struct

FeatureTupleStruct
AccessBy indexBy name
FieldsUnnamedNamed
Wire formIdenticalIdentical

Tuples and structs are byte-for-byte the same on the wire; the difference is purely in the API.

// Tuple: position matters (i32 takes int)
final pointTupleType = TupleType(<AbiType>[I32Type.type, I32Type.type]);
final point = pointTupleType.createValue(<int>[10, 20]) as TupleValue;

// Struct: names matter (i32 takes int)
final pointType = StructBuilder('Point')
.field('x', I32Type.type)
.field('y', I32Type.type)
.build();

final pointStruct = pointType.createValue(<String, dynamic>{
'x': 10, // int for i32
'y': 20,
}) as StructValue;

Complete Example

import 'package:abidock_mvx/abidock_mvx.dart';

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

// === Struct ===
final userType = StructBuilder('User')
.field('id', U64Type.type)
.field('name', StringType.type)
.field('balance', BigUIntType.type)
.field('active', BooleanType.type)
.build();

final user = userType.createValue(<String, dynamic>{
'id': BigInt.from(1),
'name': 'Alice',
'balance': BigInt.parse('1000000000000000000'),
'active': true,
}) as StructValue;

print('Full struct: ${user.nativeValue}');
print('Name: ${user.getFieldValue('name').nativeValue}');
print('Balance: ${user.getFieldValue('balance').nativeValue}');

// === Nested Struct ===
final settingsType = StructBuilder('Settings')
.field('theme', StringType.type)
.field('notifications', BooleanType.type)
.build();

final profileType = StructBuilder('UserProfile')
.field('user', userType)
.field('settings', settingsType)
.build();

final profile = profileType.createValue(<String, dynamic>{
'user': <String, dynamic>{
'id': BigInt.from(1),
'name': 'Alice',
'balance': BigInt.parse('1000000000000000000'),
'active': true,
},
'settings': <String, dynamic>{
'theme': 'dark',
'notifications': true,
},
}) as StructValue;

final innerUser = profile.getFieldValue('user') as StructValue;
final innerSettings = profile.getFieldValue('settings') as StructValue;
print('User name: ${innerUser.getFieldValue('name').nativeValue}');
print('Theme: ${innerSettings.getFieldValue('theme').nativeValue}');

// === Simple Enum ===
final statusType = EnumBuilder('Status')
.variant('Pending', 0)
.variant('Active', 1)
.variant('Completed', 2)
.build();

final status = statusType.createValue('Active') as EnumValue;
print('Variant: ${status.nativeValue}');
print('Discriminant: ${status.discriminant}');

// === Enum with Fields ===
final actionType = EnumBuilder('Action')
.variantWithFields('Transfer', 0, <AbiType>[
AddressType.type,
BigUIntType.type,
])
.variant('Withdraw', 1)
.build();

final action = actionType.createValue(<String, dynamic>{
'variant': 'Transfer',
'fields': <dynamic>[
'erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu', // bech32
BigInt.from(1000),
],
}) as EnumValue;

final actionData = action.nativeValue as Map<String, dynamic>;
print('Variant: ${actionData['variant']}');
print('Fields: ${actionData['fields']}');

// === Tuple ===
final tupleType = TupleType(<AbiType>[
U64Type.type,
BooleanType.type,
StringType.type,
]);
final tuple = tupleType.createValue(<dynamic>[
BigInt.from(42),
true,
'data',
]) as TupleValue;

print('Values: ${tuple.nativeValue}');
print('First: ${tuple.elements[0].nativeValue}');
print('Second: ${tuple.elements[1].nativeValue}');
print('Third: ${tuple.elements[2].nativeValue}');
}

Working with Contract Results

Struct from Query

final result = await controller.query(
endpointName: 'getUser',
arguments: <dynamic>[BigInt.from(1)],
);

// Native form: a Map keyed by field name
final userMap = result.first as Map<String, dynamic>;
final name = userMap['name'] as String;
final balance = userMap['balance'] as BigInt;

// Typed form, when you want the ABI metadata
final user = result.typedValues.first as StructValue;
print(user.type.name); // 'User'

Enum from Query

final result = await controller.query(
endpointName: 'getStatus',
arguments: <dynamic>[],
);

// A unit variant decodes to its name; a variant with fields to a Map.
final dynamic status = result.first;
if (status == 'Active') {
print('Contract is active');
} else if (status is Map<String, dynamic>) {
final dynamic variant = status['variant'];
final dynamic fields = status['fields'];
print('$variant with $fields');
}

Next Steps