Working with Metadata
Understanding Runtime Metadata
Runtime metadata is a comprehensive description of a blockchain's capabilities. It contains everything your application needs to know to interact with the chain, including:
- Available pallets (modules)
- Storage structure and types
- Callable functions (extrinsics)
- Events that can be emitted
- Constants defined in the runtime
- Type definitions for all data structures
Metadata Versions
The substrate_metadata package supports two metadata versions:
Metadata V14
The original production-ready metadata format:
import 'package:substrate_metadata/metadata/metadata.dart';
// Decode V14 metadata
final prefixed = RuntimeMetadataPrefixed.codec.decode(input);
if (prefixed.metadata is RuntimeMetadataV14) {
final v14 = prefixed.metadata as RuntimeMetadataV14;
print('Pallets: ${v14.pallets.length}');
print('Types: ${v14.types.length}');
}
Metadata V15
Enhanced metadata format with additional features:
- Runtime APIs metadata
- Custom metadata section
- Outer enums for better type organization
if (prefixed.metadata is RuntimeMetadataV15) {
final v15 = prefixed.metadata as RuntimeMetadataV15;
print('Runtime APIs: ${v15.apis.length}');
print('Outer enums: ${v15.outerEnums.callType}');
}
Metadata Structure
RuntimeMetadataPrefixed
The top-level container for all metadata:
class RuntimeMetadataPrefixed {
final int magicNumber; // Should be 0x6d657461 ('meta')
final RuntimeMetadata metadata; // V14 or V15
bool get isValidMagicNumber => magicNumber == 0x6d657461;
}
Example usage:
// Decode metadata
final input = Input.fromBytes(metadataBytes);
final prefixed = RuntimeMetadataPrefixed.codec.decode(input);
// Validate
if (!prefixed.isValidMagicNumber) {
throw Exception('Invalid metadata magic number');
}
// Get version
print('Metadata version: ${prefixed.metadata.version}');
Pallet Metadata
Each pallet in the runtime has its own metadata:
// Get pallet by name
final systemPallet = metadata.pallets.firstWhere(
(p) => p.name == 'System',
);
print('Pallet index: ${systemPallet.index}');
print('Has storage: ${systemPallet.storage != null}');
print('Has calls: ${systemPallet.calls != null}');
print('Has events: ${systemPallet.event != null}');
print('Number of constants: ${systemPallet.constants.length}');
Storage Metadata
Storage items define how data is stored on-chain:
final storage = systemPallet.storage;
for (final entry in storage.entries) {
print('Storage name: ${entry.name}');
print('Modifier: ${entry.modifier}'); // Optional, Default, or Required
// Check storage type
switch (entry.type) {
case StorageEntryTypePlain(:final valueType):
print('Plain storage, value type ID: $valueType');
break;
case StorageEntryTypeMap(:final hashers, :final keyType, :final valueType):
print('Map storage');
print('Key type ID: $keyType');
print('Value type ID: $valueType');
print('Hashers: ${hashers.map((h) => h.name).join(", ")}');
break;
}
}
Event Metadata
Events that can be emitted by pallets:
final eventMetadata = systemPallet.event;
if (eventMetadata != null) {
print('Event type ID: ${eventMetadata.type}');
// Get the actual event variants from the type registry
final eventType = registry.typeById(eventMetadata.type);
if (eventType.type.typeDef is TypeDefVariant) {
final variants = (eventType.type.typeDef as TypeDefVariant).variants;
for (final variant in variants) {
print('Event: ${variant.name}');
print('Fields: ${variant.fields.length}');
}
}
}
Call Metadata
Callable functions (extrinsics) defined by pallets:
final callMetadata = systemPallet.calls;
if (callMetadata != null) {
print('Call type ID: ${callMetadata.type}');
// Get the actual call variants
final callType = registry.typeById(callMetadata.type);
if (callType.type.typeDef is TypeDefVariant) {
final variants = (callType.type.typeDef as TypeDefVariant).variants;
for (final variant in variants) {
print('Call: ${variant.name}');
print('Parameters: ${variant.fields.length}');
}
}
}
Constants Metadata
Runtime constants defined by pallets:
for (final constant in systemPallet.constants) {
print('Constant: ${constant.name}');
print('Type ID: ${constant.type}');
print('Docs: ${constant.docs.join(" ")}');
// Decode the constant value
final codec = registry.codecFor(constant.type);
final value = codec.decode(Input.fromBytes(constant.value));
print('Value: $value');
}
Extrinsic Metadata
Metadata about the transaction format:
final extrinsicMetadata = metadata.extrinsic;
print('Version: ${extrinsicMetadata.version}');
print('Address type: ${extrinsicMetadata.addressType}');
print('Call type: ${extrinsicMetadata.callType}');
print('Signature type: ${extrinsicMetadata.signatureType}');
// Signed extensions (transaction extras)
for (final ext in extrinsicMetadata.signedExtensions) {
print('Extension: ${ext.identifier}');
print('Type: ${ext.type}');
print('Additional signed: ${ext.additionalSigned}');
}
V15 Specific Features
Runtime APIs
V15 metadata includes information about runtime APIs:
if (metadata is RuntimeMetadataV15) {
for (final api in metadata.apis) {
print('API: ${api.name}');
for (final method in api.methods) {
print(' Method: ${method.name}');
print(' Inputs: ${method.inputs.length}');
print(' Output type: ${method.output}');
}
}
}
Outer Enums
V15 provides top-level enum types for better organization:
if (metadata is RuntimeMetadataV15) {
final outerEnums = metadata.outerEnums;
print('Call type: ${outerEnums.callType}');
print('Event type: ${outerEnums.eventType}');
print('Error type: ${outerEnums.errorType}');
}
Complete Example
Here's a comprehensive example that explores all metadata:
import 'package:substrate_metadata/substrate_metadata.dart';
void exploreMetadata(Uint8List metadataBytes) {
// Decode metadata
final input = Input.fromBytes(metadataBytes);
final prefixed = RuntimeMetadataPrefixed.codec.decode(input);
final registry = MetadataTypeRegistry(prefixed);
print('=== Metadata Information ===');
print('Version: ${registry.version}');
print('Magic number valid: ${prefixed.isValidMagicNumber}');
print('');
print('=== Pallets ===');
for (final pallet in registry.pallets) {
print('Pallet: ${pallet.name} (index: ${pallet.index})');
// Storage
if (pallet.storage != null) {
print(' Storage items: ${pallet.storage!.entries.length}');
}
// Calls
if (pallet.calls != null) {
final callType = registry.typeById(pallet.calls!.type);
if (callType.type.typeDef is TypeDefVariant) {
final variants = (callType.type.typeDef as TypeDefVariant).variants;
print(' Calls: ${variants.length}');
}
}
// Events
if (pallet.event != null) {
final eventType = registry.typeById(pallet.event!.type);
if (eventType.type.typeDef is TypeDefVariant) {
final variants = (eventType.type.typeDef as TypeDefVariant).variants;
print(' Events: ${variants.length}');
}
}
// Constants
print(' Constants: ${pallet.constants.length}');
print('');
}
print('=== Extrinsic Format ===');
final extrinsic = registry.extrinsic;
print('Version: ${extrinsic.version}');
print('Signed extensions: ${extrinsic.signedExtensions.length}');
print('');
// V15 specific
if (registry.version == 15) {
final metadata = prefixed.metadata as RuntimeMetadataV15;
print('=== Runtime APIs (V15) ===');
for (final api in metadata.apis) {
print('${api.name}: ${api.methods.length} methods');
}
}
}
Best Practices
Cache the Registry
Creating aMetadataTypeRegistry is expensive. Create it once and reuse it for all operations.Validate Metadata
Always check the magic number and version before using metadata.Handle Version Differences
Not all chains use V15. Always check the version and handle both V14 and V15.Use ChainInfo
For common operations, use theChainInfo facade instead of working with raw metadata directly.Next Steps
- Type Registry - Learn about type resolution and codec management
- Decoding Data - Decode events and extrinsics
- Storage Access - Work with chain storage