Substrate Metadata
Decoding Events and Extrinsics
Using derived codecs to decode blockchain data
Derived Codecs Overview
The substrate_metadata package provides specialized "derived codecs" for common blockchain data structures. These codecs are built dynamically from the metadata and handle all the complexity of decoding events, extrinsics, and calls.
Available Codecs
- EventsRecordCodec: Decode event records from storage
- ExtrinsicsCodec: Decode extrinsics from blocks
- RuntimeCallCodec: Encode/decode runtime calls
- ConstantsCodec: Access pallet constants
- RuntimeEventsCodec: Decode individual events
- UncheckedExtrinsicCodec: Decode individual extrinsics
Quick Start with ChainInfo
The easiest way to use codecs is through the ChainInfo facade:
import 'package:substrate_metadata/substrate_metadata.dart';
final chainInfo = ChainInfo.fromMetadata(metadataBytes);
// Decode events from storage
final events = chainInfo.decodeEvents(eventBytes);
// Decode extrinsics from a block
final extrinsics = chainInfo.decodeExtrinsics(extrinsicBytes);
// Get a constant
final value = chainInfo.getConstant('System', 'BlockHashCount');
Decoding Events
Event Record Structure
An event record contains:
class EventRecord {
final Phase phase; // When the event occurred
final RuntimeEvent event; // The actual event
final List<String> topics; // Indexed topics
}
Using EventsRecordCodec
// Create codec from registry
final registry = MetadataTypeRegistry(prefixed);
final eventsCodec = EventsRecordCodec(registry);
// Decode events from storage
final input = Input.fromBytes(eventBytes);
final events = eventsCodec.decode(input);
// Process events
for (final eventRecord in events) {
print('Phase: ${eventRecord.phase}');
final event = eventRecord.event;
print('Event: ${event.pallet}.${event.name}');
print('Fields: ${event.fields}');
print('Topics: ${eventRecord.topics}');
}
Event Phase
The phase indicates when during block execution the event occurred:
switch (eventRecord.phase) {
case ApplyExtrinsic(:final index):
print('Occurred during extrinsic $index');
case Finalization():
print('Occurred during finalization');
case Initialization():
print('Occurred during initialization');
}
Runtime Event Structure
class RuntimeEvent {
final String pallet; // e.g., "System", "Balances"
final String name; // e.g., "Transfer", "ExtrinsicSuccess"
final Map<String, dynamic> fields; // Event data
}
Accessing Event Data
for (final eventRecord in events) {
final event = eventRecord.event;
// Check event type
if (event.pallet == 'Balances' && event.name == 'Transfer') {
final from = event.fields['from'];
final to = event.fields['to'];
final amount = event.fields['amount'];
print('Transfer: $from -> $to: $amount');
}
if (event.pallet == 'System' && event.name == 'ExtrinsicSuccess') {
final dispatchInfo = event.fields['dispatch_info'];
print('Extrinsic succeeded: $dispatchInfo');
}
}
Decoding Extrinsics
Extrinsic Structure
An extrinsic (transaction) contains:
class UncheckedExtrinsic {
final int version; // Extrinsic version
final ExtrinsicSignature? signature; // Signature (if signed)
final RuntimeCall call; // The actual call
bool get isSigned => signature != null;
}
Using ExtrinsicsCodec
// Create codec
final extrinsicsCodec = ExtrinsicsCodec(registry);
// Decode extrinsics from a block
final input = Input.fromBytes(blockExtrinsicsBytes);
final extrinsics = extrinsicsCodec.decode(input);
// Process extrinsics
for (final extrinsic in extrinsics) {
print('Version: ${extrinsic.version}');
print('Signed: ${extrinsic.isSigned}');
final call = extrinsic.call;
print('Call: ${call.pallet}.${call.name}');
print('Arguments: ${call.fields}');
if (extrinsic.signature != null) {
final sig = extrinsic.signature!;
print('Signer: ${sig.address}');
print('Signature: ${sig.signature}');
print('Extra: ${sig.extra}');
}
}
Extrinsic Signature
For signed extrinsics:
class ExtrinsicSignature {
final dynamic address; // Signer address
final dynamic signature; // Cryptographic signature
final Map<String, dynamic> extra; // Signed extensions (nonce, tip, etc.)
}
Example:
if (extrinsic.isSigned) {
final sig = extrinsic.signature!;
// Access signed extensions
final nonce = sig.extra['CheckNonce'];
final tip = sig.extra['ChargeTransactionPayment'];
final era = sig.extra['CheckMortality'];
print('Nonce: $nonce');
print('Tip: $tip');
print('Era: $era');
}
Runtime Call Structure
class RuntimeCall {
final String pallet; // e.g., "Balances"
final String name; // e.g., "transfer_keep_alive"
final Map<String, dynamic> fields; // Call arguments
}
Accessing Call Data
final call = extrinsic.call;
if (call.pallet == 'Balances' && call.name == 'transfer_keep_alive') {
final dest = call.fields['dest'];
final value = call.fields['value'];
print('Transfer to $dest: $value');
}
if (call.pallet == 'Staking' && call.name == 'bond') {
final value = call.fields['value'];
final payee = call.fields['payee'];
print('Bond $value, payee: $payee');
}
Encoding Runtime Calls
You can also encode calls for creating transactions:
// Create codec
final callsCodec = RuntimeCallCodec(registry);
// Encode a call
final call = RuntimeCall(
pallet: 'Balances',
name: 'transfer_keep_alive',
fields: {
'dest': accountId,
'value': BigInt.from(1000000000000),
},
);
final encoded = callsCodec.encode(call);
// Use this encoded call in an extrinsic
Working with Constants
Using ConstantsCodec
// Create codec
final constantsCodec = ConstantsCodec(registry);
// Get a constant (lazy access)
final lazyConstant = constantsCodec['System']['BlockHashCount'];
// Decode the value
final value = lazyConstant.value;
print('BlockHashCount: $value');
Direct Access
Or use the registry directly:
final blockHashCount = registry.getConstantValue('System', 'BlockHashCount');
final existentialDeposit = registry.getConstantValue('Balances', 'ExistentialDeposit');
Complete Example: Block Processing
Here's a complete example that processes a block:
import 'package:substrate_metadata/substrate_metadata.dart';
class BlockProcessor {
final ChainInfo chainInfo;
BlockProcessor(this.chainInfo);
void processBlock({
required Uint8List extrinsicsBytes,
required Uint8List eventsBytes,
}) {
// Decode extrinsics
final extrinsics = chainInfo.decodeExtrinsics(extrinsicsBytes);
print('Block has ${extrinsics.length} extrinsics');
// Decode events
final events = chainInfo.decodeEvents(eventsBytes);
print('Block has ${events.length} events');
// Process each extrinsic
for (var i = 0; i < extrinsics.length; i++) {
final extrinsic = extrinsics[i];
print('\n=== Extrinsic $i ===');
print('Signed: ${extrinsic.isSigned}');
print('Call: ${extrinsic.call.pallet}.${extrinsic.call.name}');
if (extrinsic.isSigned) {
final sig = extrinsic.signature!;
print('Signer: ${sig.address}');
print('Nonce: ${sig.extra['CheckNonce']}');
}
// Find events for this extrinsic
print('Events:');
for (final eventRecord in events) {
if (eventRecord.phase is ApplyExtrinsic) {
final phase = eventRecord.phase as ApplyExtrinsic;
if (phase.index == i) {
final event = eventRecord.event;
print(' ${event.pallet}.${event.name}');
// Check for failure
if (event.pallet == 'System' && event.name == 'ExtrinsicFailed') {
print(' ⚠️ Extrinsic failed!');
print(' Error: ${event.fields}');
}
}
}
}
}
// Show finalization events
print('\n=== Finalization Events ===');
for (final eventRecord in events) {
if (eventRecord.phase is Finalization) {
final event = eventRecord.event;
print('${event.pallet}.${event.name}');
}
}
}
}
// Usage
void main() async {
final chainInfo = ChainInfo.fromMetadata(metadataBytes);
final processor = BlockProcessor(chainInfo);
processor.processBlock(
extrinsicsBytes: blockExtrinsics,
eventsBytes: blockEvents,
);
}
Advanced: Custom Event Filtering
class EventFilter {
final ChainInfo chainInfo;
EventFilter(this.chainInfo);
List<RuntimeEvent> findTransfers(Uint8List eventsBytes) {
final events = chainInfo.decodeEvents(eventsBytes);
return events
.map((record) => record.event)
.where((event) =>
event.pallet == 'Balances' &&
(event.name == 'Transfer' || event.name == 'Deposit'))
.toList();
}
List<RuntimeEvent> findFailures(Uint8List eventsBytes) {
final events = chainInfo.decodeEvents(eventsBytes);
return events
.map((record) => record.event)
.where((event) =>
event.pallet == 'System' &&
event.name == 'ExtrinsicFailed')
.toList();
}
Map<int, List<RuntimeEvent>> groupEventsByExtrinsic(Uint8List eventsBytes) {
final events = chainInfo.decodeEvents(eventsBytes);
final grouped = <int, List<RuntimeEvent>>{};
for (final record in events) {
if (record.phase is ApplyExtrinsic) {
final index = (record.phase as ApplyExtrinsic).index;
grouped.putIfAbsent(index, () => []).add(record.event);
}
}
return grouped;
}
}
Working with Specific Event Types
Balance Transfers
void processTransferEvents(List<EventRecord> events) {
for (final record in events) {
final event = record.event;
if (event.pallet == 'Balances' && event.name == 'Transfer') {
final from = event.fields['from'];
final to = event.fields['to'];
final amount = event.fields['amount'];
print('Transfer: $from -> $to');
print('Amount: $amount');
}
}
}
Staking Events
void processStakingEvents(List<EventRecord> events) {
for (final record in events) {
final event = record.event;
if (event.pallet == 'Staking') {
switch (event.name) {
case 'Bonded':
print('${event.fields['stash']} bonded ${event.fields['amount']}');
case 'Unbonded':
print('${event.fields['stash']} unbonded ${event.fields['amount']}');
case 'Rewarded':
print('${event.fields['stash']} rewarded ${event.fields['amount']}');
}
}
}
}
Performance Considerations
Cache Codecs
Create codecs once and reuse them for multiple decode operations.Use ChainInfo
For most use cases, useChainInfo instead of creating codecs directly.Large Event Lists
If you're processing many events, consider streaming or pagination to avoid loading everything into memory.Next Steps
- Storage Hashers - Generate storage keys
- Metadata Merkleization - Secure offline signing
- ChainInfo API - High-level convenience API