Skip to content

Event type registration

This page explains how stored events are turned back into Dart objects, what libspiffy registers for you, and what you register yourself.

libspiffy keeps every wallet, invoice and payment channel change as an event in an Eventador event store. Each event is stored as CBOR together with its type name. When the store is read back (an aggregate recovering after a restart, a projection catching up) Eventador looks the type name up in its EventRegistry to find the factory that rebuilds the event. A type name with no factory fails with:

ArgumentError: Event type XYZ not registered

That error means a stored event’s type was never registered in this process. The usual causes are an event class of your own that you did not register, a type name that changed, or a test that called EventRegistry.clear() and did not register libspiffy’s types again.

LibSpiffyActorSystem.initialize() registers every event type libspiffy writes to its journal before it starts the projections. It calls the static method LibSpiffyActorSystem.registerEventTypes(), which covers:

  • wallet events (BitcoinWalletAggregate): wallet created, deleted and reconfigured, addresses, UTXOs and their reservations, transactions, deferred payments;
  • invoice events (InvoiceAggregate): created, paid, expired, cancelled;
  • payment channel events (PaymentChannelAggregate): requested, accepted, funded, opened, payments, closing, closed, expired, refunds.

It also registers a few types nothing emits any more (for example UTXOReservationPlacedEvent and InvoiceStatusChangedEvent), because older journals contain them and a journal is never rewritten.

registerEventTypes() is idempotent. Call it yourself when you read a libspiffy journal without starting the actor system: in a test, a migration tool, or a custom event store.

import 'package:libspiffy/libspiffy.dart';
void main() {
LibSpiffyActorSystem.registerEventTypes();
// Now an EventStore opened on a libspiffy journal can be read.
}

Every libspiffy event class declares a stableTypeName constant and returns it from typeName. That is the name stored with each event, for example:

Event class Stored type name
WalletCreatedEvent wallet.created
AddressGeneratedEvent wallet.address.generated
UTXOReceivedEvent wallet.utxo.received
UTXOSpentEvent wallet.utxo.spent
TransactionRecordedEvent wallet.transaction.recorded
TransactionSpendDeferredEvent wallet.transaction.spend_deferred
InvoiceCreatedEvent invoice.created
InvoicePaidEvent invoice.paid
ChannelOpenedEvent channel.opened
ChannelClosedEvent channel.closed

These are the domain events from package:libspiffy/internals.dart, not the coordinator events of the same class name that arrive on coordinatorEvents. Coordinator events are never stored, so they need no registration.

A stable name does not depend on the Dart class name. The journal keeps loading after a class is renamed, and in an app built with --obfuscate, where class names are scrambled. Journals written by earlier releases stored the class name (UTXOReceivedEvent); registerEventTypes() registers each class name as an alias of the stable name, so those rows still load.

You need this only if your own code writes events to an Eventador store, for example a custom aggregate next to libspiffy’s (see Custom commands & events). Add eventador to your dependencies, give the event a stable type name, and register it before anything reads the store, which in practice means before initialize().

lib/loyalty_events.dart
import 'package:eventador/eventador.dart';
class LoyaltyPointsAwarded extends Event {
/// Stored with every event. Never change it.
static const String stableTypeName = 'myapp.loyalty.points_awarded';
final String walletId;
final int points;
LoyaltyPointsAwarded({
required this.walletId,
required this.points,
super.eventId,
super.timestamp,
super.version,
super.metadata,
});
@override
String get typeName => stableTypeName;
@override
Map<String, dynamic> toMap() => {
...super.toMap(),
'walletId': walletId,
'points': points,
};
factory LoyaltyPointsAwarded.fromMap(Map<String, dynamic> map) =>
LoyaltyPointsAwarded(
walletId: map['walletId'] as String,
points: map['points'] as int,
eventId: map['eventId'] as String?,
timestamp: DateTime.parse(map['timestamp'] as String),
version: map['version'] as int?,
metadata: Map<String, dynamic>.from(map['metadata'] as Map? ?? {}),
);
}
lib/main.dart
import 'package:eventador/eventador.dart';
import 'package:libspiffy/libspiffy.dart';
import 'loyalty_events.dart';
Future<void> main() async {
EventRegistry.register<LoyaltyPointsAwarded>(
LoyaltyPointsAwarded.stableTypeName,
LoyaltyPointsAwarded.fromMap,
);
final libspiffy = LibSpiffyActorSystem();
await libspiffy.initialize(networkType: 'test', enableP2P: false);
}

Follow the same rules libspiffy does:

  • Pick a namespaced name (myapp. …) that cannot collide with libspiffy’s wallet., invoice. and channel. names.
  • Never change a stable name once events carrying it are stored.
  • If you rename a stored name anyway, register the old one as an alias: EventRegistry.register<T>(newName, fromMap, aliases: const ['OldName']), or EventRegistry.registerAlias('OldName', newName).

To check what is registered, EventRegistry.isRegistered(name) and EventRegistry.getRegisteredTypes() are available from Eventador.