substrate_metadata 2.0.0 copy "substrate_metadata: ^2.0.0" to clipboard
substrate_metadata: ^2.0.0 copied to clipboard

A package able to provide metadata types, encode and decode storage entries, extrinsics, events, and constants for Substrate runtimes.

substrate_metadata #

A powerful Dart/Flutter library for encoding and decoding Substrate blockchain metadata, extrinsics, events, and constants.


Installation #

Add to your pubspec.yaml:

dependencies:
  substrate_metadata: any

Then run:

dart pub get

Quick Start #

import 'package:substrate_metadata/substrate_metadata.dart';

// 1. Decode metadata from hex bytes
final metadataBytes = decodeHex('0x6d657461...'); // Your chain's metadata
final prefixed = RuntimeMetadataPrefixed.fromBytes(metadataBytes);

// 2. Create ChainInfo - your one-stop shop for chain operations
final chainInfo = prefixed.buildChainInfo();

// 3. Decode events from storage
final eventsHex = '0x08000000...'; // Events from System.Events storage
final events = chainInfo.eventsCodec.decode(Input.fromHex(eventsHex));

for (final record in events) {
  print('${record.event.palletName}.${record.event.eventName}');
  print('Data: ${record.event.data}');
}

// 4. Access runtime constants
final existentialDeposit = chainInfo.constantsCodec.getConstant(
  'Balances',
  'ExistentialDeposit'
);
print('Existential Deposit: $existentialDeposit');

Common Use Cases #

Decoding Events #

final eventsCodec = EventsRecordCodec(registry);

// Decode events from System.Events storage
final events = eventsCodec.decode(Input.fromHex(eventsHex));

for (final record in events) {
  // Phase: when the event occurred
  switch (record.phase.type) {
    case PhaseType.applyExtrinsic:
      print('During extrinsic ${record.phase.extrinsicIndex}');
    case PhaseType.finalization:
      print('During finalization');
    case PhaseType.initialization:
      print('During initialization');
  }

  // Event details
  final event = record.event;
  print('Pallet: ${event.palletName} (index: ${event.palletIndex})');
  print('Event: ${event.eventName} (index: ${event.eventIndex})');
  print('Data: ${event.data}');

  // Topics for filtering
  print('Topics: ${record.topics}');
}

// Encode events back to bytes
final output = HexOutput();
eventsCodec.encodeTo(events, output);
final encodedHex = output.toString();

// Query event metadata
final transferInfo = eventsCodec.getEventInfo('Balances', 'Transfer');
print('Fields: ${transferInfo?.fields.map((f) => f.name)}');

Decoding Extrinsics #

final extrinsicsCodec = ExtrinsicsCodec(registry);

// Decode extrinsics from a block
final extrinsics = extrinsicsCodec.decode(Input.fromHex(blockExtrinsicsHex));

for (final ext in extrinsics) {
  print('Version: ${ext.version}');
  print('Signed: ${ext.isSigned}');

  // The call being made
  final call = ext.call;
  print('Call: ${call.palletName}.${call.callName}');
  print('Args: ${call.args}');

  // Signature (if signed)
  if (ext.signature != null) {
    final sig = ext.signature!;
    print('Signer: ${sig.address}');
    print('Nonce: ${sig.extra['CheckNonce']}');
    print('Tip: ${sig.extra['ChargeTransactionPayment']}');
  }
}

// Encode a runtime call
final callCodec = RuntimeCallCodec(registry);
final encodedCall = callCodec.encode(RuntimeCall(
  palletName: 'Balances',
  palletIndex: 5,
  callName: 'transfer_allow_death',
  callIndex: 0,
  args: {
    'dest': {'Id': recipientBytes},
    'value': BigInt.from(1000000000000),
  },
));

Working with Constants #

// Standard codec - loads all constants eagerly
final constantsCodec = ConstantsCodec(registry);

// Get a single constant
final blockHashCount = constantsCodec.getConstant('System', 'BlockHashCount');

// Get all constants for a pallet
final balancesConstants = constantsCodec.getPalletConstants('Balances');
print('ExistentialDeposit: ${balancesConstants['ExistentialDeposit']}');

// Get all constants across runtime
final allConstants = constantsCodec.getAllConstants();

// Get constant metadata
final info = constantsCodec.getConstantInfo('Balances', 'ExistentialDeposit');
print('Type: ${info?.typeString}');
print('Docs: ${info?.documentation}');

// Lazy codec - loads constants on demand (memory efficient)
final lazyCodec = LazyConstantsCodec(registry);

// Preload frequently used constants
lazyCodec.preloadConstants([
  ('Balances', 'ExistentialDeposit'),
  ('TransactionPayment', 'TransactionByteFee'),
]);

// Check loading status
final stats = lazyCodec.getLoadingStats();
print('Loaded: ${stats['loadedConstants']}/${stats['totalConstants']}');

ChainInfo & MetadataTypeRegistry #

// ChainInfo - convenient facade for all chain operations
final chainInfo = ChainInfo.fromMetadata(metadataBytes);

// Or from prefixed metadata
final prefixed = RuntimeMetadataPrefixed.fromBytes(bytes);
final chainInfo = prefixed.buildChainInfo();

// Access components
final registry = chainInfo.registry;           // Type registry
final eventsCodec = chainInfo.eventsCodec;     // Events codec
final extrinsicsCodec = chainInfo.extrinsicsCodec;  // Extrinsics codec
final constantsCodec = chainInfo.constantsCodec;    // Constants codec
final callsCodec = chainInfo.callsCodec;       // Runtime calls codec

// MetadataTypeRegistry - the heart of the type system
final registry = MetadataTypeRegistry(prefixedMetadata);

// Get codec for any type ID
final codec = registry.codecFor(typeId);
final decoded = codec.decode(input);

// Look up types
final type = registry.typeById(42);
final typeByPath = registry.typeByPath('sp_runtime::DispatchError');

// Access pallet metadata
final balances = registry.palletByName('Balances');
final system = registry.palletByIndex(0);

// Get storage metadata
final accountStorage = registry.getStorageMetadata('System', 'Account');

// V15: Access runtime APIs
final apiCodec = registry.getRuntimeApiOutputCodec('Core', 'version');

Features #

  • Metadata V14 & V15 Support - Full support for modern Substrate metadata formats
  • Complete Type System - Portable type registry with all Substrate type definitions
  • Event Decoding/Encoding - Decode and encode runtime events with full type safety
  • Extrinsic Handling - Parse and construct signed/unsigned extrinsics
  • Constants Access - Easy access to runtime constants with lazy loading support
  • Storage Hashers - Blake2b, Blake3, and XXHash implementations for storage key generation
  • Cross-Platform - Works on Flutter, Dart VM, and Web (WASM compatible)
  • Zero Configuration - Automatically adapts to any Substrate-based chain via metadata

Storage Hashers #

Generate storage keys for querying chain state:

// Available hashers
final blake2b128 = Hasher.blake2b128;  // Cryptographic, 16 bytes
final blake2b256 = Hasher.blake2b256;  // Cryptographic, 32 bytes
final twox128 = Hasher.twoxx128;       // Fast (non-crypto), 16 bytes
final twox64 = Hasher.twoxx64;         // Fast (non-crypto), 8 bytes

// Hash a string
final hash = Hasher.twoxx128.hashString('System');

// Storage value (no key)
final storageValue = StorageValue(
  prefix: 'System',
  storage: 'Number',
  valueCodec: U32Codec.codec,
);
final key = storageValue.hashedKey();  // 32 bytes

// Storage map (single key)
final storageMap = StorageMap(
  prefix: 'System',
  storage: 'Account',
  hasher: StorageHasher.blake2b128Concat(accountIdCodec),
  valueCodec: accountInfoCodec,
);
final accountKey = storageMap.hashedKeyFor(accountId);

// Storage double map (two keys)
final doubleMap = StorageDoubleMap(
  prefix: 'Staking',
  storage: 'ErasStakers',
  hasher1: StorageHasher.twoxx64Concat(U32Codec.codec),
  hasher2: StorageHasher.twoxx64Concat(accountIdCodec),
  valueCodec: exposureCodec,
);
final stakersKey = doubleMap.hashedKeyFor(eraIndex, validatorId);

// N-map for variable number of keys
final nMap = StorageNMap(
  prefix: 'Assets',
  storage: 'Account',
  hashers: [
    StorageHasher.blake2b128Concat(U32Codec.codec),
    StorageHasher.blake2b128Concat(accountIdCodec),
  ],
  valueCodec: assetAccountCodec,
);
final assetKey = nMap.hashedKeyFor([assetId, accountId]);

Hasher Selection Guide #

Hasher Security Speed Use Case
blake2b128Concat Secure Moderate User-controlled keys (recommended)
blake2b256 Secure Moderate High-security hashing
twox64Concat Not secure Fast Trusted keys only
twox128 Not secure Fast Internal/system keys
identity N/A Fastest When key is already unique

Note: "Concat" variants append the original key, enabling storage enumeration.

Data Models #

RuntimeEvent #

class RuntimeEvent {
  final String palletName;    // e.g., "Balances"
  final int palletIndex;      // e.g., 5
  final String eventName;     // e.g., "Transfer"
  final int eventIndex;       // e.g., 2
  final Map<String, dynamic> data;  // Event fields

  String get identifier => '$palletName.$eventName';
}

UncheckedExtrinsic #

class UncheckedExtrinsic {
  final int version;                    // Extrinsic version (usually 4)
  final ExtrinsicSignature? signature;  // null for unsigned
  final RuntimeCall call;               // The actual call

  bool get isSigned => signature != null;
}

RuntimeCall #

class RuntimeCall {
  final String palletName;
  final int palletIndex;
  final String callName;
  final int callIndex;
  final Map<String, dynamic> args;
}

EventRecord #

class EventRecord {
  final Phase phase;           // When during block execution
  final RuntimeEvent event;    // The actual event
  final List<String> topics;   // For indexed filtering
}

Phase #

enum PhaseType { applyExtrinsic, finalization, initialization }

class Phase {
  final PhaseType type;
  final int? extrinsicIndex;  // Only for applyExtrinsic
}

Core Concepts #

The Metadata-Driven Approach #

Unlike traditional SDKs that require compile-time type definitions, substrate_metadata dynamically builds codecs from runtime metadata. This means:

  1. Any Chain Support - Works with Polkadot, Kusama, or any custom Substrate chain
  2. Version Agnostic - Automatically handles runtime upgrades
  3. No Code Generation - No build steps or generated files needed

Key Components #

Component Purpose
RuntimeMetadataPrefixed Decoded metadata with version detection
MetadataTypeRegistry Core type resolution and codec generation
ChainInfo High-level facade for common operations
EventsRecordCodec Decode/encode event records
ExtrinsicsCodec Decode/encode block extrinsics
ConstantsCodec Access runtime constants

Metadata Versions #

V14 (Current Standard) #

  • Portable type registry
  • Pallet metadata (calls, events, errors, constants, storage)
  • Extrinsic metadata with signed extensions

V15 (Latest) #

Everything in V14, plus:

  • Runtime APIs: Metadata for runtime API methods
  • Outer Enums: Direct access to RuntimeCall, RuntimeEvent, RuntimeError types
  • Custom Metadata: Chain-specific metadata extensions
  • Pallet Documentation: Documentation strings for pallets
// V15-specific: Access runtime APIs
if (registry.outerEnums != null) {
  final apiCodec = registry.getRuntimeApiOutputCodec('TransactionPaymentApi', 'query_info');
  // Use codec for runtime API calls
}

Breaking Changes from 2.0.0 Version #

This is a major rewrite of the substrate_metadata package. If you're upgrading from a pre 2.0.0 version, please read this section carefully.

Removed: Legacy Metadata Support (V9-V13) #

The new implementation only supports Metadata V14 and V15. Support for legacy metadata versions V9 through V13 has been removed.

// OLD: Legacy versions were supported
final metadata = MetadataDecoder.instance.decode(hexString); // Worked with V9-V15

// NEW: Only V14 and V15 are supported
final prefixed = RuntimeMetadataPrefixed.fromBytes(bytes); // V14 or V15 only

Impact: If you're connecting to chains running very old runtimes (pre-V14), you'll need to upgrade those chains or use the legacy version of this package.

Removed: Chain Class and Spec Version Management #

The Chain class and the entire spec version management system have been removed. The old API managed multiple metadata versions indexed by block number.

// OLD API (removed)
final chain = Chain();
await chain.initSpecVersionFromFile('spec_versions.json');
final versionDesc = chain.getVersionDescription(blockNumber);
final decoded = chain.decodeExtrinsics(rawBlockExtrinsics);
final events = chain.decodeEvents(rawBlockEvents);

// NEW API
final prefixed = RuntimeMetadataPrefixed.fromBytes(metadataBytes);
final chainInfo = prefixed.buildChainInfo();
final decoded = chainInfo.extrinsicsCodec.decode(input);
final events = chainInfo.eventsCodec.decode(input);

Migration: Create a new ChainInfo instance when the chain's metadata changes (i.e., after a runtime upgrade).

Removed: MetadataDecoder Singleton #

The MetadataDecoder.instance singleton pattern has been replaced with direct static factory methods.

// OLD API (removed)
final decoded = MetadataDecoder.instance.decode(hexString);
final version = decoded.version;
final metadata = decoded.metadata;

// NEW API
final prefixed = RuntimeMetadataPrefixed.fromHex(hexString);
// or
final prefixed = RuntimeMetadataPrefixed.fromBytes(bytes);
final metadata = prefixed.metadata; // RuntimeMetadataV14 or RuntimeMetadataV15

Removed: DecodedMetadata, RawBlockExtrinsics, RawBlockEvents #

These wrapper classes have been removed. The new API works directly with codecs and typed models.

// OLD API (removed)
final rawBlock = RawBlockExtrinsics(blockNumber: 123, extrinsics: hexList);
final decoded = chain.decodeExtrinsics(rawBlock);  // DecodedBlockExtrinsics

// NEW API
final extrinsics = chainInfo.extrinsicsCodec.decode(Input.fromHex(blockExtrinsicsHex));
// Returns List<UncheckedExtrinsic> with strongly typed RuntimeCall

Removed: Constant<T> Generic Class #

The generic Constant<T> class with lazy value decoding has been replaced with ConstantsCodec.

// OLD API (removed)
final chainInfo = ChainInfo.fromMetadata(decoded, legacyTypes);
final constant = chainInfo.constants['Balances']!['ExistentialDeposit']!;
final value = constant.value;  // Decoded on access
final type = constant.type;    // Codec<T>
final docs = constant.docs;

// NEW API
final chainInfo = prefixed.buildChainInfo();
final value = chainInfo.constantsCodec.getConstant('Balances', 'ExistentialDeposit');
final info = chainInfo.constantsCodec.getConstantInfo('Balances', 'ExistentialDeposit');
// info.typeString, info.documentation, info.palletName, etc.

// For memory-efficient lazy loading:
final lazyCodec = LazyConstantsCodec(registry);
final value = lazyCodec.getConstant('Balances', 'ExistentialDeposit');

Removed: LegacyTypes, LegacyTypesBundle, LegacyParser #

All legacy type system components have been removed since V9-V13 support was dropped.

// OLD API (removed)
final legacyTypes = LegacyTypes(types: {...}, typesAlias: {...});
final chainInfo = ChainInfo.fromMetadata(decoded, legacyTypes);

Migration: These are no longer needed with V14+ metadata since all type information is self-contained in the metadata.

Removed: Parsers (V14Parser, TypeExpressionParser, TypeNormalizer) #

The parsers have been replaced with MetadataTypeRegistry which handles all type resolution internally.

// OLD API (removed)
final chainInfo = V14Parser.getChainInfo(decodedMetadata);

// NEW API
final registry = MetadataTypeRegistry(prefixedMetadata);
final codec = registry.codecFor(typeId);

Changed: Event Decoding Returns Strongly Typed Models #

// OLD API - returned List<dynamic>
final events = chain.decodeEvents(rawBlockEvents);
for (final event in events) {
  final pallet = event['pallet'];
  final name = event['name'];
  final data = event['data'];
}

// NEW API - returns List<EventRecord>
final events = chainInfo.eventsCodec.decode(input);
for (final record in events) {
  final pallet = record.event.palletName;    // String
  final name = record.event.eventName;       // String
  final data = record.event.data;            // Map<String, dynamic>
  final phase = record.phase;                // Phase (typed enum)
  final topics = record.topics;              // List<String>
}

Changed: Extrinsic Decoding Returns Strongly Typed Models #

// OLD API - returned List<Map>
final extrinsics = chain.decodeExtrinsics(rawBlockExtrinsics);
for (final ext in extrinsics) {
  final hash = ext['hash'];
  final calls = ext['calls'];
  final signature = ext['signature'];
}

// NEW API - returns List<UncheckedExtrinsic>
final extrinsics = chainInfo.extrinsicsCodec.decode(input);
for (final ext in extrinsics) {
  final version = ext.version;           // int
  final isSigned = ext.isSigned;         // bool
  final call = ext.call;                 // RuntimeCall
  final signature = ext.signature;       // ExtrinsicSignature?

  // Access call details
  print('${call.palletName}.${call.callName}');
  print('Args: ${call.args}');
}

Changed: Type System Restructured #

The scale_info/ folder has been replaced with metadata/common/type_def/.

// OLD imports (removed)
import 'package:substrate_metadata/scale_info/scale_info.dart';
// TypeDef, TypeDefComposite, TypeDefVariant, Field, Variant, etc.

// NEW imports
import 'package:substrate_metadata/substrate_metadata.dart';
// Same types, different location in metadata/common/

Changed: Dependencies Reduced #

The package now has fewer dependencies:

Removed Kept Added
convert pointycastle hashlib_codecs
equatable polkadart_scale_codec
json_schema
polkadart
utility
cross_file

Changed: Minimum Dart SDK Version #

# OLD
environment:
  sdk: ^3.6.0

# NEW
environment:
  sdk: ^3.8.0

New: Built-in Substrate Hashers #

Storage hashers are now built into this package.

import 'package:substrate_metadata/substrate_metadata.dart';

// Available hashers
final hash = Hasher.blake2b128.hash(bytes);
final hash = Hasher.blake2b256.hash(bytes);
final hash = Hasher.twoxx64.hash(bytes);
final hash = Hasher.twoxx128.hash(bytes);

// Storage key generation
final storageMap = StorageMap(
  prefix: 'System',
  storage: 'Account',
  hasher: StorageHasher.blake2b128Concat(accountIdCodec),
  valueCodec: accountInfoCodec,
);
final key = storageMap.hashedKeyFor(accountId);

New: LazyConstantsCodec for Memory Optimization #

// Load constants on-demand instead of all at once
final lazyCodec = LazyConstantsCodec(registry);

// Preload frequently used constants
lazyCodec.preloadConstants([
  ('Balances', 'ExistentialDeposit'),
  ('System', 'BlockHashCount'),
]);

// Check loading statistics
final stats = lazyCodec.getLoadingStats();
print('Loaded: ${stats['loadedConstants']}/${stats['totalConstants']}');

Resources #

License #

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

2
likes
140
points
2.15k
downloads

Publisher

verified publisherpolkadart.dev

Weekly Downloads

A package able to provide metadata types, encode and decode storage entries, extrinsics, events, and constants for Substrate runtimes.

Homepage
Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

hashlib_codecs, pointycastle, polkadart_scale_codec

More

Packages that depend on substrate_metadata