Polkadart Logo
SCALE Codec

Composite Types

Building complex data structures with composite codecs and enums

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