Polkadart Logo
Substrate Metadata

Type Registry

Understanding and using the MetadataTypeRegistry for type resolution and codec management

What is the Type Registry?

The MetadataTypeRegistry is the central component for working with types in Substrate metadata. It acts as a type resolution engine that:

  • Resolves type IDs to their definitions
  • Creates and caches codecs for encoding/decoding
  • Handles recursive types safely
  • Provides efficient lookups for pallets and their components

Think of it as a smart factory that knows how to build the right codec for any type in the metadata.

Creating a Registry

import 'package:substrate_metadata/substrate_metadata.dart';

// From raw metadata bytes
final input = Input.fromBytes(metadataBytes);
final prefixed = RuntimeMetadataPrefixed.codec.decode(input);
final registry = MetadataTypeRegistry(prefixed);

// Or use ChainInfo which includes a registry
final chainInfo = ChainInfo.fromMetadata(metadataBytes);
final registry = chainInfo.registry;

The registry automatically:

  • Validates the magic number
  • Checks version compatibility (V14 or V15)
  • Builds pallet indices for O(1) lookups
  • Initializes caching structures

Core Type Resolution

Getting Codecs by Type ID

The most fundamental operation is getting a codec for a type:

// Get codec for a specific type ID
final codec = registry.codecFor(42);

// Use the codec to decode data
final value = codec.decode(input);

// Or encode data
final encoded = codec.encode(myData);

The registry automatically handles:

  • Primitive types (u8, u32, bool, etc.)
  • Composite types (structs)
  • Variant types (enums)
  • Sequences (Vec)
  • Arrays
  • Tuples
  • Compact encoding
  • Recursive types

Type Caching

Codecs are expensive to create, so the registry caches them:

// First call - creates and caches
final codec1 = registry.codecFor(42);

// Second call - returns cached version
final codec2 = registry.codecFor(42);

// Same instance
assert(identical(codec1, codec2));

// Check cache statistics
final stats = registry.cacheStats;
print('Cached codecs: ${stats.codecCacheSize}');
print('Cached paths: ${stats.typePathCacheSize}');

Handling Recursive Types

The registry safely handles recursive types using a proxy pattern:

// Example: A linked list type that references itself
// The registry detects recursion and uses ProxyCodec
final listCodec = registry.codecFor(linkedListTypeId);

// The codec works correctly despite the recursion
final list = listCodec.decode(input);

Type Lookups

By Type ID

Direct lookup by type ID:

final portableType = registry.typeById(42);

print('Type path: ${portableType.type.path.join("::")}');
print('Type params: ${portableType.type.params.length}');

// Access the type definition
switch (portableType.type.typeDef) {
  case TypeDefPrimitive(:final primitive):
    print('Primitive: ${primitive.name}');
  case TypeDefComposite(:final fields):
    print('Composite with ${fields.length} fields');
  case TypeDefVariant(:final variants):
    print('Enum with ${variants.length} variants');
  // ... other cases
}

By Type Path

Lookup by the type's full path (slower, but cached):

// Find type by path
final optionType = registry.typeByPath('Option');
final resultType = registry.typeByPath('Result');
final accountIdType = registry.typeByPath('sp_core::crypto::AccountId32');

// Get codec by path
final codec = registry.codecForPath('Option');
Path lookups are expensive on first call (linear search through all types). Results are cached for subsequent calls.

Pallet Access

The registry provides efficient pallet lookups:

By Name (O(1))

final systemPallet = registry.palletByName('System');
final balancesPallet = registry.palletByName('Balances');

if (systemPallet != null) {
  print('Index: ${systemPallet.index}');
  print('Storage items: ${systemPallet.storage?.entries.length ?? 0}');
}

By Index (O(1))

final pallet = registry.palletByIndex(0); // Usually System

All Pallets

for (final pallet in registry.pallets) {
  print('${pallet.name} (${pallet.index})');
}

Pallet Component Types

Get type IDs for pallet components:

// Calls type
final callTypeId = registry.getPalletCallType('Balances');
if (callTypeId != null) {
  final callCodec = registry.codecFor(callTypeId);
}

// Events type
final eventTypeId = registry.getPalletEventType('System');

// Errors type
final errorTypeId = registry.getPalletErrorType('Balances');

Storage Access

Storage Metadata

Get metadata for a storage entry:

final storage = registry.getStorageMetadata('System', 'Account');

if (storage != null) {
  print('Name: ${storage.name}');
  print('Modifier: ${storage.modifier}');
  print('Default: ${storage.defaultValue}');

  // Check storage type
  switch (storage.type) {
    case StorageEntryTypePlain(:final valueType):
      print('Plain storage, value type: $valueType');

    case StorageEntryTypeMap(:final hashers, :final keyType, :final valueType):
      print('Map storage');
      print('Key type: $keyType');
      print('Value type: $valueType');
      print('Hashers: $hashers');
  }
}

Storage Type ID

Get just the type ID:

final typeId = registry.getStorageType('System', 'Account');
if (typeId != null) {
  final codec = registry.codecFor(typeId);
  // Use codec to decode storage value
}

Storage Hashers

Get the hashers used for a storage map:

final hashers = registry.getStorageHashers('System', 'Account');
// hashers contains the list of StorageHasherEnum values

Constants Access

Constant Metadata

final constant = registry.getConstantMetadata('System', 'BlockHashCount');

if (constant != null) {
  print('Name: ${constant.name}');
  print('Type: ${constant.type}');
  print('Documentation: ${constant.docs.join(" ")}');
}

Direct Value Access

Get the decoded constant value directly:

final blockHashCount = registry.getConstantValue('System', 'BlockHashCount');
print('BlockHashCount: $blockHashCount');

final existentialDeposit = registry.getConstantValue('Balances', 'ExistentialDeposit');
print('ExistentialDeposit: $existentialDeposit');

Variant and Composite Resolution

Get Variant by Name

For enum types:

final callType = registry.getPalletCallType('Balances');
if (callType != null) {
  final transferVariant = registry.getVariant(callType, 'transfer');

  if (transferVariant != null) {
    print('Variant index: ${transferVariant.index}');
    print('Fields: ${transferVariant.fields.length}');

    for (final field in transferVariant.fields) {
      print('Field: ${field.name ?? "unnamed"}, type: ${field.type}');
    }
  }
}

Get Variant by Index

final variant = registry.getVariantByIndex(callType, 0);

Get Composite Fields

For struct types:

final fields = registry.getFields(typeId);

if (fields != null) {
  for (final field in fields) {
    print('Field: ${field.name ?? "unnamed"}');
    print('Type: ${field.type}');
    print('Type name: ${field.typeName}');
  }
}

Extrinsic & Common Types

Extrinsic Metadata

final extrinsic = registry.extrinsic;

print('Version: ${extrinsic.version}');
print('Address type: ${registry.accountIdType}');
print('Signature type: ${registry.signatureType}');
print('Call type: ${registry.callType}');
print('Extra type: ${registry.extraType}');

Signed Extensions

final extensions = registry.signedExtensions;

for (final ext in extensions) {
  print('Extension: ${ext.identifier}');
  print('Type: ${ext.type}');
  print('Additional signed: ${ext.additionalSigned}');
}

V15 Specific Features

When working with V15 metadata:

Outer Enums

final outerEnums = registry.outerEnums;

if (outerEnums != null) {
  final callCodec = registry.codecFor(outerEnums.callType);
  final eventCodec = registry.codecFor(outerEnums.eventType);
  final errorCodec = registry.codecFor(outerEnums.errorType);
}

Runtime APIs

final contractsApi = registry.getRuntimeApi('ContractsApi');

if (contractsApi != null) {
  print('Methods: ${contractsApi.methods.length}');

  for (final method in contractsApi.methods) {
    print('Method: ${method.name}');
  }
}

Runtime API Methods

final method = registry.getRuntimeApiMethod('ContractsApi', 'call');

if (method != null) {
  print('Inputs: ${method.inputs.length}');
  print('Output type: ${method.output}');

  // Get output codec
  final outputCodec = registry.codecFor(method.output);
}

Or use the convenience method:

final codec = registry.getRuntimeApiOutputCodec('ContractsApi', 'call');

if (codec != null) {
  final result = codec.decode(responseBytes);
}

Utility Methods

Type Checking

// Check if a type is Option<T>
if (registry.isOption(typeId)) {
  print('This is an Option type');
}

// Check if a type is Result<T, E>
if (registry.isResult(typeId)) {
  print('This is a Result type');
}

// Check if a type is Vec<T>
if (registry.isSequence(typeId)) {
  print('This is a sequence/Vec type');
}

Cache Management

// Get cache statistics
final stats = registry.cacheStats;
print('Total cached items: ${stats.totalCachedItems}');
print('Codecs: ${stats.codecCacheSize}');
print('Type paths: ${stats.typePathCacheSize}');
print('Proxy codecs: ${stats.proxyCodecCount}');

// Clear all caches (rarely needed)
registry.clearCache();

Complete Example

Here's a comprehensive example using the registry:

import 'package:substrate_metadata/substrate_metadata.dart';

void demonstrateRegistry(Uint8List metadataBytes) {
  // Create registry
  final chainInfo = ChainInfo.fromMetadata(metadataBytes);
  final registry = chainInfo.registry;

  print('=== Basic Info ===');
  print('Version: ${registry.version}');
  print('Pallets: ${registry.pallets.length}');

  print('\n=== Decode Account Info ===');
  // Get the Account storage type
  final accountTypeId = registry.getStorageType('System', 'Account');
  if (accountTypeId != null) {
    final codec = registry.codecFor(accountTypeId);
    final accountInfo = codec.decode(accountDataInput);
    print('Account data: $accountInfo');
  }

  print('\n=== Decode an Event ===');
  final eventTypeId = registry.outerEnums?.eventType ??
      registry.getPalletEventType('System');

  if (eventTypeId != null) {
    final eventCodec = registry.codecFor(eventTypeId);
    // Use the codec to decode events
  }

  print('\n=== Access Constants ===');
  final ss58Prefix = registry.getConstantValue('System', 'SS58Prefix');
  print('SS58 Prefix: $ss58Prefix');

  final existentialDeposit = registry.getConstantValue('Balances', 'ExistentialDeposit');
  print('Existential Deposit: $existentialDeposit');

  print('\n=== Cache Statistics ===');
  final stats = registry.cacheStats;
  print('Cached codecs: ${stats.codecCacheSize}');
  print('Cached paths: ${stats.typePathCacheSize}');
}

Performance Tips

Reuse the Registry

Create the registry once and reuse it. Creating a new registry is expensive.

Prefer Type IDs

When possible, use type IDs instead of paths. ID lookups are O(1), path lookups are O(n).

Cache Lookup Results

If you're looking up the same pallet/storage/constant repeatedly, cache the results.

Use ChainInfo for Common Operations

For standard operations, use ChainInfo instead of directly using the registry.

Next Steps