Polkadart Logo
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, use ChainInfo 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