Composite Types
Composite Types Overview
Composite types allow you to build complex data structures from primitive codecs. They're similar to structs or classes in programming languages, enabling you to encode and decode multi-field objects.
Composite Codec
The CompositeCodec encodes and decodes structures (objects with named fields).
Basic Composite
import 'package:polkadart_scale_codec/polkadart_scale_codec.dart';
// Define a structure: {id: u32, active: bool, name: String}
final codec = CompositeCodec({
'id': U32Codec.codec,
'active': BoolCodec.codec,
'name': StrCodec.codec,
});
// Encode
final data = {
'id': 42,
'active': true,
'name': 'Alice',
};
final output = ByteOutput();
codec.encodeTo(data, output);
// Decode
final decoded = codec.decode(Input.fromBytes(output.toBytes()));
print(decoded); // {id: 42, active: true, name: Alice}
Encoding Order
Fields are encoded in the order they appear in the codec definition:
final codec = CompositeCodec({
'field1': U32Codec.codec, // Encoded first
'field2': BoolCodec.codec, // Encoded second
'field3': StrCodec.codec, // Encoded third
});
// Encoding of {field1: 100, field2: true, field3: "hi"}
// [100, 0, 0, 0, 1, 8, 104, 105]
// └────┬────┘ └┘ └─────┬────┘
// field1 field2 field3
Nested Composites
Composites can contain other composites:
// Address structure
final addressCodec = CompositeCodec({
'street': StrCodec.codec,
'city': StrCodec.codec,
'zipCode': U32Codec.codec,
});
// Person structure with nested address
final personCodec = CompositeCodec({
'id': U32Codec.codec,
'name': StrCodec.codec,
'address': addressCodec, // Nested composite
});
final person = {
'id': 1,
'name': 'Alice',
'address': {
'street': '123 Main St',
'city': 'Springfield',
'zipCode': 12345,
},
};
final encoded = personCodec.encode(person);
Empty Composites
An empty composite encodes to zero bytes:
final emptyCodec = CompositeCodec({});
final output = ByteOutput();
emptyCodec.encodeTo({}, output);
print(output.toBytes()); // [] - empty!
Composite with Collections
// User with tags (Vec<String>)
final userCodec = CompositeCodec({
'id': U32Codec.codec,
'name': StrCodec.codec,
'tags': SequenceCodec(StrCodec.codec),
});
final user = {
'id': 1,
'name': 'Alice',
'tags': ['admin', 'verified', 'premium'],
};
final encoded = userCodec.encode(user);
Enum Codecs
SCALE supports two types of enums: simple (unit variants) and complex (variants with data).
Simple Enum
Simple enums have variants without associated data:
// enum Status { Active, Inactive, Pending }
final codec = SimpleEnumCodec.fromList([
'Active', // Index 0
'Inactive', // Index 1
'Pending', // Index 2
]);
// Encode
final output = ByteOutput();
codec.encodeTo('Active', output);
print(output.toBytes()); // [0]
codec.encodeTo('Pending', output);
print(output.toBytes()); // [0, 2]
// Decode
final decoded = codec.decode(Input.fromBytes([1]));
print(decoded); // 'Inactive'
Sparse Simple Enum
For enums with gaps in indices:
// enum Status {
// Active = 0,
// Pending = 2, // Skip 1
// Archived = 5, // Skip 3, 4
// }
final codec = SimpleEnumCodec.sparse({
0: 'Active',
2: 'Pending',
5: 'Archived',
});
// Encoding
codec.encodeTo('Pending', output); // [2]
codec.encodeTo('Archived', output); // [5]
Complex Enum
Complex enums have variants with associated data:
// enum Message {
// Text(String),
// Number(u32),
// Tuple(u32, bool),
// }
final codec = ComplexEnumCodec.fromList([
MapEntry('Text', StrCodec.codec),
MapEntry('Number', U32Codec.codec),
MapEntry('Tuple', TupleCodec([U32Codec.codec, BoolCodec.codec])),
]);
// Encode Text variant
final output1 = ByteOutput();
codec.encodeTo(MapEntry('Text', 'Hello'), output1);
// [0, 20, 72, 101, 108, 108, 111]
// └┘ └──────────┬──────────────┘
// idx "Hello" string
// Encode Number variant
final output2 = ByteOutput();
codec.encodeTo(MapEntry('Number', 42), output2);
// [1, 42, 0, 0, 0]
// └┘ └────┬────┘
// idx u32 value
// Encode Tuple variant
final output3 = ByteOutput();
codec.encodeTo(MapEntry('Tuple', [100, true]), output3);
// [2, 100, 0, 0, 0, 1]
// └┘ └──────┬───────┘
// idx tuple data
Decoding Complex Enums
final input = Input.fromHex('0x0114426f62');
final decoded = codec.decode(input);
print(decoded.key); // 'Number'
print(decoded.value); // 42
Sparse Complex Enum
For enums with non-sequential indices:
// enum Event {
// Created(u32) = 0,
// Updated(String) = 5,
// Deleted = 10,
// }
final codec = ComplexEnumCodec.sparse({
0: MapEntry('Created', U32Codec.codec),
5: MapEntry('Updated', StrCodec.codec),
10: MapEntry('Deleted', NullCodec()),
});
Real-World Examples
Account Information
// AccountInfo structure from Substrate
final accountInfoCodec = CompositeCodec({
'nonce': U32Codec.codec,
'consumers': U32Codec.codec,
'providers': U32Codec.codec,
'sufficients': U32Codec.codec,
'data': CompositeCodec({
'free': U128Codec.codec,
'reserved': U128Codec.codec,
'frozen': U128Codec.codec,
'flags': U128Codec.codec,
}),
});
final accountInfo = {
'nonce': 5,
'consumers': 0,
'providers': 1,
'sufficients': 0,
'data': {
'free': BigInt.from(1000000000000),
'reserved': BigInt.zero,
'frozen': BigInt.zero,
'flags': BigInt.zero,
},
};
final encoded = accountInfoCodec.encode(accountInfo);
Transaction Call
// Transfer call: balances.transfer(dest, amount)
final transferCodec = CompositeCodec({
'dest': U8ArrayCodec(32), // AccountId32
'value': CompactBigIntCodec.codec,
});
final call = {
'dest': List.generate(32, (i) => i), // Destination account
'value': BigInt.from(1000000000000), // 1 DOT
};
final encoded = transferCodec.encode(call);
Event Record
// Event with different types
final eventCodec = ComplexEnumCodec.fromList([
MapEntry('Transfer', CompositeCodec({
'from': U8ArrayCodec(32),
'to': U8ArrayCodec(32),
'amount': U128Codec.codec,
})),
MapEntry('Deposit', CompositeCodec({
'who': U8ArrayCodec(32),
'amount': U128Codec.codec,
})),
MapEntry('Withdraw', CompositeCodec({
'who': U8ArrayCodec(32),
'amount': U128Codec.codec,
})),
]);
// Transfer event
final transferEvent = MapEntry('Transfer', {
'from': List.filled(32, 1),
'to': List.filled(32, 2),
'amount': BigInt.from(1000000000000),
});
final encoded = eventCodec.encode(transferEvent);
Complete Example: Blockchain Data
import 'package:polkadart_scale_codec/polkadart_scale_codec.dart';
// Define a blockchain extrinsic structure
class Extrinsic {
final int version;
final ExtrinsicSignature? signature;
final Call call;
Extrinsic(this.version, this.signature, this.call);
}
class ExtrinsicSignature {
final List<int> address;
final List<int> signature;
final int nonce;
final BigInt tip;
ExtrinsicSignature(this.address, this.signature, this.nonce, this.tip);
}
class Call {
final String pallet;
final String method;
final Map<String, dynamic> args;
Call(this.pallet, this.method, this.args);
}
// Codec for signature
final signatureCodec = CompositeCodec({
'address': U8ArrayCodec(32),
'signature': U8ArrayCodec(64),
'nonce': CompactCodec.codec,
'tip': CompactBigIntCodec.codec,
});
// Codec for call arguments (varies by call)
final transferArgsCodec = CompositeCodec({
'dest': U8ArrayCodec(32),
'value': CompactBigIntCodec.codec,
});
// Codec for call
final callCodec = ComplexEnumCodec.fromList([
MapEntry('Balances.transfer', transferArgsCodec),
// Add more call types as needed
]);
// Codec for complete extrinsic
final extrinsicCodec = CompositeCodec({
'version': U8Codec.codec,
'signature': OptionCodec(signatureCodec),
'call': callCodec,
});
void main() {
// Create a signed transfer extrinsic
final extrinsic = {
'version': 4,
'signature': {
'address': List.filled(32, 1),
'signature': List.filled(64, 2),
'nonce': 5,
'tip': BigInt.zero,
},
'call': MapEntry('Balances.transfer', {
'dest': List.filled(32, 3),
'value': BigInt.from(1000000000000),
}),
};
// Encode
final encoded = extrinsicCodec.encode(extrinsic);
print('Encoded size: ${encoded.length} bytes');
// Decode
final decoded = extrinsicCodec.decode(Input.fromBytes(encoded));
print('Version: ${decoded['version']}');
print('Has signature: ${decoded['signature'] != null}');
}
Design Patterns
Builder Pattern
class PersonBuilder {
static final _codec = CompositeCodec({
'id': U32Codec.codec,
'name': StrCodec.codec,
'age': U8Codec.codec,
'email': OptionCodec(StrCodec.codec),
});
int? id;
String? name;
int? age;
String? email;
PersonBuilder setId(int id) {
this.id = id;
return this;
}
PersonBuilder setName(String name) {
this.name = name;
return this;
}
PersonBuilder setAge(int age) {
this.age = age;
return this;
}
PersonBuilder setEmail(String? email) {
this.email = email;
return this;
}
Uint8List build() {
return _codec.encode({
'id': id!,
'name': name!,
'age': age!,
'email': email,
});
}
}
// Usage
final encoded = PersonBuilder()
.setId(1)
.setName('Alice')
.setAge(30)
.setEmail('[email protected]')
.build();
Type-Safe Wrappers
class AccountId {
final List<int> bytes;
const AccountId(this.bytes);
}
class AccountIdCodec with Codec<AccountId> {
final _inner = U8ArrayCodec(32);
const AccountIdCodec();
@override
void encodeTo(AccountId value, Output output) {
_inner.encodeTo(value.bytes, output);
}
@override
AccountId decode(Input input) {
return AccountId(_inner.decode(input));
}
@override
bool isSizeZero() => false;
}
// Usage with composites
final transferCodec = CompositeCodec({
'from': AccountIdCodec(),
'to': AccountIdCodec(),
'amount': U128Codec.codec,
});
Best Practices
Maintain Field Order
Always maintain the same field order in your codec definition as the data structure:// Good
final codec = CompositeCodec({
'id': U32Codec.codec,
'name': StrCodec.codec,
});
// Order matters for encoding/decoding
Use Descriptive Names
Use clear, descriptive names for enum variants and struct fields:// Good
ComplexEnumCodec.fromList([
MapEntry('UserCreated', userCodec),
MapEntry('UserUpdated', userCodec),
MapEntry('UserDeleted', idCodec),
]);
// Avoid
ComplexEnumCodec.fromList([
MapEntry('A', userCodec),
MapEntry('B', userCodec),
MapEntry('C', idCodec),
]);
Don't Skip Validation
Always validate data before encoding:void encodeUser(Map<String, dynamic> user) {
if (!user.containsKey('id')) {
throw Exception('Missing required field: id');
}
userCodec.encodeTo(user, output);
}
Cache Complex Codecs
Create complex codecs once and reuse:// Good
class Codecs {
static final account = CompositeCodec({...});
static final transfer = ComplexEnumCodec.fromList([...]);
}
// Avoid recreating on every use
Size Considerations
Composite Size
Composite size is the sum of all fields:
// {id: u32, active: bool, name: String}
// With data: {id: 42, active: true, name: "Alice"}
// Size: 4 + 1 + (1 + 5) = 11 bytes
// └─┘ └┘ └──┬──┘
// u32 bool compact(5) + "Alice"
Enum Size
Enum size is 1 byte (index) + variant data:
// enum Message { Text(String), Number(u32) }
// Text("Hi"): 1 + (1 + 2) = 4 bytes
// Number(42): 1 + 4 = 5 bytes
Next Steps
- Input/Output - Master the I/O system
- Best Practices - Learn encoding best practices
- Examples - See real-world usage examples