Skip to content

twostack/libspiffy

Repository files navigation

LibSpiffy - Event-Sourced Bitcoin Wallet

An actor-based Bitcoin wallet implementation using event sourcing, CQRS, and SPV (Simplified Payment Verification) built with the Dactor/Eventador/DuraQ stack.

Architecture Overview

LibSpiffy implements a sophisticated Bitcoin wallet system using modern architectural patterns:

  • Event Sourcing: All wallet state changes are captured as immutable events
  • CQRS: Clear separation between commands (write operations) and queries (read operations)
  • Actor Model: Concurrent, fault-tolerant processing using Dactor
  • SPV: Lightweight Bitcoin verification using merkle proofs
  • Hybrid Stack: Combines Dactor (actors), Eventador (event store), and DuraQ (workflows)

Key Features

Core Wallet Functionality

  • HD wallet address generation and management
  • Wallet import (xpub watch-only, WIF private key)
  • UTXO tracking with confirmation status
  • Transaction creation and signing
  • SPV transaction verification with merkle proofs (BEEF/BUMP)
  • Multi-wallet support with isolation
  • Event-sourced state with full audit trail

Advanced Features

  • Invoice-based payment system for simplified SPV validation
  • Multi-output invoices (P2PKH, P2MS multisig, OP_RETURN metadata, Plugin-delegated outputs)
  • Plugin system for custom script types and token protocols (ScriptPlugin, TransactionBuilderPlugin)
  • Payment channels for off-chain micropayments with on-chain settlement
  • Benford distribution UTXO splitting for transaction privacy
  • UTXO holds and reservations with automatic cleanup
  • Transaction lifecycle management with pending transaction recovery
  • Snapshot support for performance optimization
  • Real-time balance calculations
  • ARC (Authoritative Response Component) service integration for broadcasting and fee estimation
  • Transaction fee calculation from BEEF data

Storage & Deployment

  • Isar embedded database for mobile/desktop
  • PostgreSQL backend for server-side deployments
  • In-memory storage for development and testing
  • AES-256-GCM encrypted key storage (xpub/xpriv)
  • Pluggable storage interfaces for custom backends

Network Integration

  • Bitcoin P2P network connectivity via SpiffyNode
  • Block header synchronization (P2P network and CDN-based fast sync)
  • BEEF (Background Evaluation Extended Format) transaction validation
  • BUMP (BSV Universal Merkle Path) merkle proof validation
  • Merkle proof validation against header chain
  • ARC (Authoritative Response Component) service integration
  • WhatsOnChain blockchain data source for wallet import

System Architecture

LibSpiffy implements a CQRS (Command Query Responsibility Segregation) architecture with complete separation between write operations (commands → events → EventStore) and read operations (queries → ReadModels).

┌───────────────────────────────────────────────────────────────────────────┐
│                        LibSpiffy CQRS Architecture                        │
├───────────────────────────────────────────────────────────────────────────┤
│                                                                           │
│  PUBLIC API (import 'package:libspiffy/coordinator.dart')                 │
│  ┌────────────────────────────────────────────────────────────────────┐   │
│  │  WalletCoordinatorActor (Unified Facade)                           │   │
│  │  • Send: CreateWalletCommand, PayInvoiceCommand, GetBalanceQuery   │   │
│  │  • Recv: WalletCreatedEvent, PaymentReadyEvent, BalanceResponse    │   │
│  │  • Handles correlation tracking, error routing, channel P2P        │   │
│  └────────────────────────────────┬───────────────────────────────────┘   │
│                                   │ Internal delegation                   │
│  COMMAND SIDE (Write Operations)  │                                       │
│  ┌──────────────────┐      ┌──────┴─────────────┐                         │
│  │ Wallet Manager   │─────▶│ Invoice Coordinator│                         │
│  │ Actor            │      │ Actor              │                         │
│  │ • Routes cmds    │      │ • Routes invoice   │                         │
│  │ • Spawns aggr.   │      │   commands         │                         │
│  │ • Multi-wallet   │      │ • Spawns invoice   │                         │
│  └────────┬─────────┘      │   aggregates       │                         │
│           │                └──────────┬─────────┘                         │
│           ▼                           ▼                                   │
│  ┌────────────────┐        ┌────────────────┐                             │
│  │ Wallet         │        │ Invoice        │                             │
│  │ Aggregate      │        │ Aggregate      │                             │
│  │ • Validates    │        │ • Validates    │                             │
│  │ • Emits events │        │ • Emits events │                             │
│  └────────┬───────┘        └────────┬───────┘                             │
│           └────────────┬────────────┘                                     │
│                        ▼                                                  │
│              ┌─────────────────┐                                          │
│              │   Event Store   │  (Write-Only from Aggregates)            │
│              │   (Eventador)   │                                          │
│              └────────┬────────┘                                          │
│  ═════════════════════╪═══════════════════════════════════════════════    │
│                       ▼  Event Stream                                     │
│              ┌─────────────────┐                                          │
│              │ Projection Mgr  │  (Read-Only from EventStore)             │
│              └────────┬────────┘                                          │
│         ┌─────────────┴─────────────┐                                     │
│         ▼                           ▼                                     │
│  ┌──────────────┐          ┌──────────────┐                               │
│  │   Wallet     │          │   Invoice    │                               │
│  │  Projection  │          │  Projection  │                               │
│  └──────┬───────┘          └──────┬───────┘                               │
│         └────────────┬────────────┘                                       │
│                      ▼                                                    │
│           ┌─────────────────────┐                                         │
│           │  Read Model Storage │  (Isar / PostgreSQL / In-Memory)        │
│           └─────────────────────┘                                         │
│                      ▲                                                    │
│  QUERY SIDE (Read Operations)                                             │
│  ┌──────────────────┐     ┌────────────────┐     ┌─────────────────┐      │
│  │   SPV Actor      │     │   ARC Actor    │     │ Header Sync     │      │
│  │ • BEEF/BUMP val. │     │ • Broadcast    │     │ • Block headers │      │
│  │ • Invoice match  │     │ • Fee estimate │     │ • Merkle proofs │      │
│  │ • Fee calc       │     │ • Policy query │     │ • Chain valid.  │      │
│  └──────────────────┘     └────────────────┘     └─────────────────┘      │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘

Key CQRS Principles in LibSpiffy

Write Side (Commands)

  • Commands routed through Coordinator Actors
  • Aggregates (event-sourced) validate and emit events
  • Events persisted to EventStore (immutable, append-only)
  • No direct storage writes by application code

Read Side (Queries)

  • Projections subscribe to EventStore event stream
  • Projections update denormalized ReadModels in Isar
  • Queries read from ReadModels (never EventStore)
  • Optimized for fast lookups without joins

Benefits

  • Performance: Reads optimized separately from writes
  • Scalability: Read/write can scale independently
  • Audit Trail: Complete event history in EventStore
  • Eventual Consistency: Projections update asynchronously
  • Flexibility: Multiple read models from same events

Quick Start

Prerequisites

  • Dart SDK 3.5.1 or later
  • Dependencies: dactor, eventador, duraq, dartsv

⚠️ Critical: Event Type Registration

Before using LibSpiffy, you must understand that all event types must be registered with Eventador's EventRegistry for proper CBOR deserialization after system restarts.

LibSpiffy handles this automatically during initialization via _registerEventTypes(), but if you're extending LibSpiffy with custom events, you'll need to register them:

import 'package:eventador/eventador.dart';

// Register custom event types BEFORE initializing LibSpiffy
void registerMyCustomEvents() {
  EventRegistry.register<MyCustomWalletEvent>(
    'MyCustomWalletEvent',
    (map) => MyCustomWalletEvent.fromMap(map),
  );
}

// Then initialize LibSpiffy
await initializeLibSpiffy(dataDirectory: './wallet-data');

What LibSpiffy registers automatically:

  • 11 Wallet events (WalletCreatedEvent, AddressGeneratedEvent, UTXOReceivedEvent, etc.)
  • 5 Invoice events (InvoiceCreatedEvent, InvoicePaidEvent, etc.)

Why this matters:

  • Events are stored in CBOR format in the EventStore
  • After restart, Eventador needs to deserialize events back into Dart objects
  • Without registration: ArgumentError: Event type 'XYZ' not registered

See the Eventador README for complete details on event registration.

Installation

# Clone the repository
git clone <repository-url>
cd libspiffy

# Install dependencies
dart pub get

# Run the example
dart run example/bitcoin_wallet_example.dart

Basic Usage (Coordinator API - Recommended)

The WalletCoordinatorActor is the canonical public interface for third-party apps. It provides a unified command/event API that handles all internal actor orchestration, correlation tracking, and async response routing.

import 'package:libspiffy/libspiffy.dart';
import 'package:libspiffy/coordinator.dart';

// Initialize LibSpiffy
final libspiffy = LibSpiffyActorSystem();
await libspiffy.initialize(
  dataDirectory: './wallet-data',
  arcConfig: ArcServiceConfig.taalMainnet(),
  enableP2P: true,
);

// Use the coordinator - THE single entry point
final coordinator = libspiffy.coordinator;

// Subscribe to events
libspiffy.coordinatorEvents?.listen((event) {
  if (event is WalletCreatedEvent) {
    print('Wallet created: ${event.walletId}');
  } else if (event is PaymentReadyEvent) {
    print('BEEF ready to send: ${event.txid}');
  } else if (event is BalanceResponse) {
    print('Balance: ${event.totalBalance} sats');
  }
});

// Send commands
coordinator.tell(CreateWalletCommand(
  walletId: 'my-wallet',
  name: 'My Bitcoin Wallet',
));

coordinator.tell(CreateInvoiceCommand(
  walletId: 'my-wallet',
  amount: BigInt.from(100000),
  description: 'Payment for services',
));

coordinator.tell(GetBalanceQuery(walletId: 'my-wallet'));

// Cleanup
await libspiffy.shutdown();

Direct Actor Access (Advanced)

For advanced use cases, you can access internal actors directly:

import 'package:libspiffy/libspiffy.dart';

await initializeLibSpiffy(dataDirectory: './wallet-data');

// Direct actor access (advanced - most apps should use the coordinator)
final walletManager = getLibSpiffySystem().walletManager;
final spvActor = getLibSpiffySystem().spvActor;

walletManager.tell(CreateWalletMessage('my-wallet', 'My Wallet'));

await shutdownLibSpiffy();

Integration with Host Actor System

If your application already uses Dactor actors, LibSpiffy can integrate seamlessly:

import 'package:dactor/dactor.dart';
import 'package:libspiffy/libspiffy.dart';

// Your application's actor system
final hostActorSystem = LocalActorSystem(ActorSystemConfig());

// Initialize LibSpiffy using your actor system
await initializeLibSpiffy(
  actorSystem: hostActorSystem,  // LibSpiffy actors join your system!
  dataDirectory: './wallet-data',
);

// Now all actors are in the same system
// You can spawn your own actors that interact with LibSpiffy
final myActor = await hostActorSystem.spawn(
  'payment-processor',
  () => PaymentProcessorActor(
    walletManager: getLibSpiffySystem().walletManager,
    invoiceCoordinator: getLibSpiffySystem().invoiceCoordinator,
  ),
);

// When shutting down, LibSpiffy won't shutdown the host's actor system
await shutdownLibSpiffy(); // Only closes LibSpiffy's resources

// Host manages its own actor system
await hostActorSystem.shutdown();

Benefits of Shared Actor System

  • Unified Supervision: Single supervision tree for all actors
  • Better Resource Efficiency: One message dispatcher instead of two
  • Clearer Failure Propagation: Unified error handling and recovery
  • Natural Actor Hierarchy: LibSpiffy actors integrate into your supervision strategy
  • Direct Communication: No cross-system message overhead

Actor System Integration Patterns

Pattern 1: Standalone Mode

Use LibSpiffy as a complete, self-contained wallet system:

// LibSpiffy manages everything
await initializeLibSpiffy(dataDirectory: './data');

// Use wallet functionality
final walletManager = getLibSpiffySystem().walletManager;
walletManager.tell(CreateWalletMessage(...));

// LibSpiffy handles its own lifecycle
await shutdownLibSpiffy();

Best for: Simple applications, microservices, or when LibSpiffy is the primary component.

Pattern 2: Integrated Mode

Integrate LibSpiffy into an existing actor-based application:

// Host application setup
final app = MyActorBasedApp();
await app.initialize();

// LibSpiffy joins the host's actor system
await initializeLibSpiffy(
  actorSystem: app.actorSystem,
  dataDirectory: './data',
);

// Your actors can directly communicate with LibSpiffy actors
final paymentProcessor = await app.actorSystem.spawn(
  'payment-processor',
  () => PaymentProcessorActor(
    walletManager: getLibSpiffySystem().walletManager,
    spvActor: getLibSpiffySystem().spvActor,
  ),
);

// Lifecycle management
await shutdownLibSpiffy();  // Closes LibSpiffy resources only
await app.shutdown();       // Host manages actor system shutdown

Best for: Complex applications with multiple actor-based subsystems.

Pattern 3: Coordinator (Built-in Gateway)

LibSpiffy provides a built-in WalletCoordinatorActor that serves as the canonical gateway. You no longer need to build your own:

import 'package:libspiffy/coordinator.dart';

// The coordinator IS the gateway - no custom actor needed
final coordinator = getLibSpiffySystem().coordinator;

// Send any command
coordinator.tell(CreateWalletCommand(walletId: 'my-wallet', name: 'My Wallet'));
coordinator.tell(PayInvoiceCommand(walletId: 'my-wallet', invoiceId: '...', ...));
coordinator.tell(GetBalanceQuery(walletId: 'my-wallet'));

// Subscribe to all events
getLibSpiffySystem().coordinatorEvents?.listen((event) {
  switch (event) {
    case WalletCreatedEvent e: handleWalletCreated(e);
    case PaymentReadyEvent e: handlePaymentReady(e);
    case BEEFValidationResultEvent e: handleBEEFValidated(e);
    case ErrorEvent e: handleError(e);
    default: break;
  }
});

Best for: All third-party integrations. This is the recommended pattern for most applications.

Checking Integration Mode

// Check if LibSpiffy owns its actor system
if (getLibSpiffySystem().ownsActorSystem) {
  print('LibSpiffy is running in standalone mode');
} else {
  print('LibSpiffy is integrated with host actor system');
}

// Access the underlying actor system if needed
final actorSystem = getLibSpiffySystem().actorSystem;

CQRS Event Sourcing Flow

LibSpiffy implements a complete CQRS (Command Query Responsibility Segregation) architecture with event sourcing. Understanding this flow is crucial for working with the system.

Complete Flow Diagram

Commands (Write)                      Events (Immutable)                 Queries (Read)
     │                                      │                                │
     ▼                                      ▼                                ▼
┌─────────────┐                    ┌──────────────┐                 ┌──────────────┐
│   Command   │                    │    Event     │                 │    Query     │
│   (Intent)  │                    │  (Fact)      │                 │  (Question)  │
└──────┬──────┘                    └──────┬───────┘                 └──────┬───────┘
       │                                  │                                │
       │ 1. Route                         │ 3. Persist                     │ 7. Read
       ▼                                  ▼                                ▼
┌─────────────────┐              ┌──────────────┐               ┌──────────────────┐
│  Coordinator    │              │  EventStore  │               │  ReadModel       │
│  Actor          │              │  (Isar CBOR) │               │  Storage (Isar)  │
│ • Wallet Mgr    │              │              │               │                  │
│ • Invoice Coord │              │ • Immutable  │               │ • Denormalized   │
└────────┬────────┘              │ • Append-only│               │ • Fast lookups   │
         │                       │ • Recovery   │               │ • No joins       │
         │ 2. Spawn/Tell         └──────┬───────┘               └────────▲─────────┘
         ▼                              │                                │
┌──────────────────┐                    │ 4. Stream                      │ 6. Update
│   Aggregate      │                    ▼                                │
│   (Domain Logic) │            ┌──────────────┐                         │
│ • Wallet         │            │  Projection  │                         │
│ • Invoice        │            │  Manager     │                         │
│                  │            │              │                         │
│ • Validate       │            │ • Subscribe  │                         │
│ • Emit Events    │◀───────────│ • Route      │                         │
└──────────────────┘ 5. Replay  │ • Checkpoint │                         │
                                └──────┬───────┘                         │
                                       │                                 │
                                       │ 5. Route by type               │
                                       ▼                                 │
                            ┌──────────────────────┐                     │
                            │    Projections       │─────────────────────┘
                            │ • WalletProjection   │
                            │ • InvoiceProjection  │
                            └──────────────────────┘

Step-by-Step Flow

Write Side (Commands → Events → EventStore)

Step 1: Command Routing

// User sends a command to coordinator
invoiceCoordinator.tell(CreateInvoiceMessage(
  walletId: 'wallet-001',
  amount: BigInt.from(100000),
  description: 'Payment for services',
));

Step 2: Aggregate Spawning/Routing

// Coordinator spawns or retrieves aggregate actor
final invoiceAggregateRef = await actorSystem.spawn(
  'Invoice_$invoiceId',
  () => InvoiceAggregate(
    persistenceId: 'Invoice_$invoiceId',
    eventStore: eventStore,
  ),
);

// Sends command to aggregate
invoiceAggregateRef.tell(CreateInvoiceCommand(...));

Step 3: Event Emission

// Inside InvoiceAggregate.handleCommand()
@override
Future<List<Event>> handleCommand(InvoiceState currentState, Command command) async {
  if (command is CreateInvoiceCommand) {
    // Validate business rules
    if (command.amount <= BigInt.zero) {
      throw ArgumentError('Amount must be positive');
    }
    
    // Return events (not state changes!)
    return [InvoiceCreatedEvent(
      invoiceId: command.invoiceId,
      walletId: command.walletId,
      addresses: command.addresses,
      amount: command.amount,
      // ... other fields
    )];
  }
}

Step 4: Event Persistence

// AggregateRoot base class automatically persists events to EventStore
// Events stored as CBOR in Isar
// This happens BEFORE eventHandler is called

Step 5: Event Application

// Inside InvoiceAggregate.eventHandler()
@override
void eventHandler(Event event) {
  ensureStateInitialized(); // Critical for recovery
  
  if (event is InvoiceCreatedEvent) {
    // Mutate currentState directly (new in Eventador)
    currentState.status = InvoiceStatus.pending;
    currentState.amount = event.amount;
    currentState.addresses = event.addresses;
    currentState.createdAt = event.timestamp;
    currentState.version++;
  }
}

Read Side (EventStore → Projections → ReadModels)

Step 6: Event Streaming

// ProjectionManager subscribes to EventStore
// Streams events to registered projections
await projectionManager.start(); // Starts event stream processing

Step 7: Projection Handling

// InvoiceProjection.handle() receives events
@override
Future<bool> handle(Event event) async {
  if (event is InvoiceCreatedEvent) {
    // Create denormalized read model
    final invoice = Invoice(
      invoiceId: event.invoiceId,
      walletId: event.walletId,
      addresses: event.addresses,
      amount: event.amount,
      status: InvoiceStatus.pending,
      createdAt: event.createdAt,
      // ... optimized for queries
    );
    
    // Write to ReadModelStorage (Isar)
    await storage.storeInvoice(invoice);
    
    // Update checkpoint for idempotent replay
    await updateCheckpoint(event.version);
    
    return true; // Event handled
  }
  return false; // Event not handled by this projection
}

Step 8: Query Execution

// Queries NEVER touch EventStore, only ReadModelStorage
invoiceCoordinator.tell(CheckInvoiceMessage(invoiceId));

// Inside coordinator
Future<void> _handleCheckInvoice(CheckInvoiceMessage msg) async {
  // Query read model storage (fast!)
  final invoice = await storage.getInvoice(msg.invoiceId);
  
  context.sender?.tell(InvoiceDetailsResponse(
    invoiceId: invoice.invoiceId,
    status: invoice.status,
    // ... all denormalized data
  ));
}

Recovery After Restart

When the system restarts, aggregates recover their state by replaying events:

// 1. Aggregate spawned during recovery
final aggregate = InvoiceAggregate(
  persistenceId: 'Invoice_abc123',
  eventStore: eventStore,
);

// 2. AggregateRoot.preStart() automatically replays events from EventStore
// 3. Events applied via eventHandler() to rebuild state
// 4. Aggregate ready to process new commands with correct state

// Projections also replay from their last checkpoint
// This ensures ReadModels are eventually consistent

Key Principles

✅ DO:

  • Route all commands through coordinators
  • Let aggregates emit events (never mutate storage directly)
  • Use projections to build read models
  • Query read models for fast lookups
  • Register event types before system startup

❌ DON'T:

  • Write to storage from aggregates or coordinators
  • Read from EventStore for queries (use ReadModels)
  • Skip event registration (causes deserialization errors)
  • Mutate events after creation (they're immutable)
  • Query EventStore for business logic

Storage Separation

LibSpiffy uses separate databases for events and read models:

EventStore (Write-Only by Aggregates)

  • Schema: EventEnvelope, SnapshotEnvelope (from Eventador)
  • Format: CBOR-serialized events
  • Access: AggregateRoot base class only
  • Purpose: Immutable audit trail, recovery

ReadModelStorage (Write-Only by Projections, Read by App)

  • Schema: InvoiceEntity, BitcoinUtxoEntity, BitcoinTransactionEntity, PaymentChannelEntity
  • Format: Denormalized domain objects
  • Access: Projections write, application reads
  • Purpose: Fast queries, optimized for reads

Storage Backends:

  • Isar (mobile/desktop) — embedded, zero-config
  • PostgreSQL (server) — connection pooling, migrations, SSL support
  • In-memory (development/testing)

This separation ensures:

  • Clear CQRS boundaries
  • Independent scaling of read/write
  • No accidental EventStore queries
  • Optimized storage formats for each use case
  • Deployment flexibility across mobile, desktop, and server

Invoice-Based SPV Payments

LibSpiffy implements a streamlined SPV payment verification system using invoices:

Creating and Paying Invoices

// 1. Receiver creates an invoice with payment addresses
final invoice = await createInvoice(
  walletId: 'bob-wallet',
  amount: BigInt.from(100000), // satoshis
  description: 'Payment for services',
  numberOfAddresses: 1, // Can request multiple addresses
);

// Invoice contains:
// - invoiceId: Unique identifier
// - addresses: Pre-generated payment addresses
// - amount: Expected payment amount
// - expiresAt: Invoice expiration time

// 2. Sender creates transaction paying to invoice address(es)
final tx = await createTransaction(
  fromWallet: 'alice-wallet',
  toAddresses: [invoice.addresses.first],
  amount: invoice.amount,
);

// 3. Sender broadcasts transaction with BEEF (includes merkle proof)
await broadcastTransaction(
  transaction: tx,
  beef: beef, // Contains tx + parent txs + merkle proof
  invoiceId: invoice.invoiceId, // Links tx to invoice
);

// 4. SPV Actor validates the transaction:
//    - Verifies merkle proof against block header chain
//    - Confirms outputs match invoice addresses
//    - Validates payment amount
//    - Calculates transaction fee from BEEF data
//    - Marks invoice as paid

// 5. Receiver's wallet is automatically updated with new UTXOs

SPV Validation Flow

  1. Transaction Received: SPV Actor receives transaction with BEEF and invoice ID
  2. Merkle Proof Validation: Validates transaction is in a valid block
  3. Invoice Lookup: Retrieves expected payment addresses from Invoice Manager
  4. Output Verification: Confirms transaction pays to invoice addresses
  5. Amount Validation: Verifies payment amount matches invoice
  6. UTXO Extraction: Identifies new spendable UTXOs and spent UTXOs
  7. Fee Calculation: Computes transaction fee from input/output values in BEEF
  8. State Update: Wallet state updated via event sourcing
  9. Invoice Marking: Invoice marked as paid

Benefits

  • Simplified Verification: No need to scan entire blockchain for transactions
  • Immediate Validation: SPV validation completes in milliseconds
  • Privacy: Addresses are single-use and linked to specific invoices
  • Security: Merkle proofs provide cryptographic assurance
  • Efficient: Only block headers needed, not full blocks

Core Components

1. Bitcoin Wallet Aggregate (Event-Sourced)

The core domain logic for wallet operations - an Eventador AggregateRoot:

// Spawned automatically by WalletManagerActor
// Each wallet is an independent aggregate actor
class BitcoinWalletAggregate extends AggregateRoot<WalletState> {
  @override
  Future<List<Event>> handleCommand(WalletState currentState, Command command) async {
    // Validate business rules and return events
    if (command is GenerateAddressCommand) {
      return [AddressGeneratedEvent(
        walletId: persistenceId,
        address: generatedAddress,
        derivationIndex: currentState.nextDerivationIndex,
        // ...
      )];
    }
    // ... other command handlers
  }
  
  @override
  void eventHandler(Event event) {
    ensureStateInitialized(); // Critical!
    
    // Mutate currentState directly based on events
    if (event is AddressGeneratedEvent) {
      currentState.addresses[event.address] = event.derivationIndex;
      currentState.nextDerivationIndex++;
      currentState.version++;
    }
    // ... other event handlers
  }
}

Key Features:

  • Event-sourced: All state changes via events
  • Validation: Business rules enforced before events
  • Recovery: Rebuilds state from events after restart
  • Snapshots: Configurable for performance

2. Wallet Manager Actor (Coordinator)

Long-lived coordinator that manages multiple wallet aggregates:

// Create wallet (spawns BitcoinWalletAggregate actor)
walletManager.tell(CreateWalletMessage(
  walletId: 'wallet-001',
  name: 'My Bitcoin Wallet',
));

// Send command to wallet aggregate
walletManager.tell(WalletCommandMessage(
  walletId: 'wallet-001',
  command: GenerateAddressCommand(
    walletId: 'wallet-001',
    metadata: {'purpose': 'receiving'},
  ),
));

// Query wallet (reads from ReadModel, not EventStore)
walletManager.tell(GetWalletBalanceMessage(
  walletId: 'wallet-001',
));

Responsibilities:

  • Routes commands to appropriate wallet aggregates
  • Spawns wallet aggregate actors on-demand
  • Manages wallet lifecycle
  • Automated UTXO reservation cleanup

3. Invoice Aggregate (Event-Sourced)

Domain logic for invoice lifecycle - an Eventador AggregateRoot:

// Spawned by InvoiceCoordinatorActor per invoice
class InvoiceAggregate extends AggregateRoot<InvoiceState> {
  @override
  Future<List<Event>> handleCommand(InvoiceState currentState, Command command) async {
    if (command is CreateInvoiceCommand) {
      return [InvoiceCreatedEvent(
        invoiceId: persistenceId,
        walletId: command.walletId,
        addresses: command.addresses,
        amount: command.amount,
        createdAt: DateTime.now(),
        // ...
      )];
    }
    
    if (command is MarkInvoicePaidCommand) {
      // Business rule validation
      if (currentState.status != InvoiceStatus.pending) {
        throw StateError('Invoice is not pending');
      }
      
      return [InvoicePaidEvent(
        invoiceId: persistenceId,
        paidAt: DateTime.now(),
        txid: command.txid,
        amountReceived: command.amountReceived,
      )];
    }
    // ... other commands
  }
  
  @override
  void eventHandler(Event event) {
    ensureStateInitialized();
    
    if (event is InvoiceCreatedEvent) {
      currentState.status = InvoiceStatus.pending;
      currentState.amount = event.amount;
      currentState.addresses = event.addresses;
      // ...
    }
    
    if (event is InvoicePaidEvent) {
      currentState.status = InvoiceStatus.paid;
      currentState.paidAt = event.paidAt;
      currentState.paymentTxid = event.txid;
      // ...
    }
  }
}

Key Features:

  • Invoice state machine (pending → paid/expired/cancelled)
  • Business rule enforcement
  • Complete audit trail of invoice lifecycle

4. Invoice Coordinator Actor (Coordinator)

Long-lived coordinator for invoice operations (replaces old InvoiceManagerActor):

// Create an invoice (spawns InvoiceAggregate, requests addresses from wallet)
invoiceCoordinator.tell(CreateInvoiceMessage(
  walletId: 'wallet-001',
  amount: BigInt.from(100000),
  description: 'Payment for services',
  numberOfAddresses: 1,
));

// Mark invoice as paid (routes to InvoiceAggregate)
invoiceCoordinator.tell(MarkInvoicePaidMessage(
  invoiceId: 'invoice-123',
  txid: 'transaction-hex',
  amountReceived: BigInt.from(100000),
  addressesPaidTo: ['address1'],
));

// Check invoice status (queries ReadModel)
invoiceCoordinator.tell(CheckInvoiceMessage(
  invoiceId: 'invoice-123',
));

// List invoices (queries ReadModel with optional filter)
invoiceCoordinator.tell(ListInvoicesMessage(
  walletId: 'wallet-001',
  status: InvoiceStatus.pending, // Optional
));

// Cancel invoice (routes to InvoiceAggregate)
invoiceCoordinator.tell(CancelInvoiceMessage(
  invoiceId: 'invoice-123',
));

Responsibilities:

  • Routes commands to InvoiceAggregate actors
  • Coordinates with WalletManager for address generation
  • Queries ReadModelStorage for invoice lookups
  • Periodic expiration checks
  • Does NOT write to storage (projections do that!)

5. Projections (Read-Side Event Handlers)

Projections listen to EventStore and update ReadModels:

// WalletProjection - Updates wallet read models
class WalletProjection extends Projection<WalletReadModel> {
  @override
  Future<bool> handle(Event event) async {
    if (event is UTXOReceivedEvent) {
      // Update denormalized UTXO view in Isar
      await storage.storeUTXO(BitcoinUtxo.fromEvent(event));
      return true;
    }
    // ... other wallet events
  }
}

// InvoiceProjection - Updates invoice read models  
class InvoiceProjection extends Projection<InvoiceReadModel> {
  @override
  Future<bool> handle(Event event) async {
    if (event is InvoiceCreatedEvent) {
      // Check for existing invoice (idempotent replay)
      final existing = await storage.getInvoice(event.invoiceId);
      if (existing == null) {
        await storage.storeInvoice(Invoice.fromEvent(event));
      }
      return true;
    }
    
    if (event is InvoicePaidEvent) {
      // Update existing invoice status
      await storage.updateInvoiceStatus(
        event.invoiceId,
        InvoiceStatus.paid,
        paidAt: event.paidAt,
        txid: event.txid,
      );
      return true;
    }
    // ... other invoice events
  }
}

Key Features:

  • Subscribes to event stream from EventStore
  • Builds denormalized read models
  • Checkpointing for idempotent replay
  • Eventual consistency (async updates)

6. Projection Manager (CQRS Orchestration)

Coordinates event streaming to all projections:

// Initialized automatically by LibSpiffyActorSystem
final projectionManager = ProjectionManager(eventStore);

// Register projections
await projectionManager.registerProjection(walletProjection);
await projectionManager.registerProjection(invoiceProjection);

// Start event streaming
await projectionManager.start();

// ProjectionManager:
// - Streams events from EventStore
// - Routes events to interested projections
// - Manages checkpoints for each projection
// - Handles projection failures gracefully

7. SPV Actor

Handles SPV transaction validation with BEEF/BUMP merkle proofs:

// Receive and validate a transaction
spvActor.tell(ReceiveTransactionMessage(
  transactionId: 'txid-hex',
  beef: beefData, // Contains tx + parents + merkle proof
  fromCounterparty: 'alice',
  targetWalletId: 'bob-wallet',
  invoiceId: 'invoice-123', // Links to invoice
));

// SPV Actor will:
// 1. Validate merkle proof against block headers
// 2. Verify outputs match invoice addresses
// 3. Calculate transaction fee
// 4. Extract spendable UTXOs
// 5. Update wallet state via commands
// 6. Mark invoice as paid

8. ARC Actor

Interfaces with ARC service for transaction broadcasting and fee estimation:

// Broadcast a transaction
arcActor.tell(BroadcastTransactionMessage(
  txid: 'transaction-id',
  rawTx: transactionHex,
));

// Estimate transaction fee
arcActor.tell(EstimateFeeMessage(
  estimatedSize: 250, // bytes
));

// Query transaction status
arcActor.tell(GetTransactionStatusMessage(
  txid: 'transaction-id',
));

// Get ARC policy (fee rates, limits)
arcActor.tell(GetPolicyMessage());

9. Block Header Sync Actor

Manages block headers and validates merkle proofs:

// Start header sync
headerSync.tell(StartHeaderSyncMessage(
  startHeight: 0,
  targetHeight: null, // null = sync to tip
));

// Validate merkle proof
headerSync.tell(ValidateMerkleProofMessage(
  requestId: 'validate-1',
  merkleProof: proof,
  txid: 'transaction-id',
));

10. Plugin System

LibSpiffy provides an extensible plugin architecture for custom Bitcoin script types and token protocols. Plugins are decoupled from the core library — no compile-time dependency on token implementations.

// Register a plugin (e.g., tstokenlib for TSL1 tokens)
final registry = PluginRegistry();
registry.register(myTokenPlugin);

// Plugins provide:
// - Script identification: recognize custom script types in UTXOs
// - Metadata extraction: parse protocol-specific data from scripts
// - Lock/unlock builders: construct locking and unlocking scripts
// - Transaction builders: build complete multi-output protocol transactions

// Send a plugin-based payment via coordinator
coordinator.tell(PayInvoiceCommand(
  walletId: 'my-wallet',
  invoiceId: 'invoice-123',
  pluginOutputs: [PluginOutputSpec(pluginId: 'tsl1', params: {...})],
));

The CallbackTransactionSigner enables plugins to sign transactions without exposing private keys — the wallet aggregate retains exclusive control of key material.

See Plugin API Guide for the full interface reference.

11. Payment Channels

Off-chain micropayment channels with on-chain funding and settlement:

// Open a channel via coordinator
coordinator.tell(OpenChannelCommand(
  walletId: 'my-wallet',
  counterpartyPubKey: counterpartyKey,
  fundingAmount: BigInt.from(1000000),
));

// Make off-chain payments
coordinator.tell(MakeChannelPaymentCommand(
  channelId: 'channel-123',
  amount: BigInt.from(1000),
));

// Close and settle on-chain
coordinator.tell(CloseChannelCommand(channelId: 'channel-123'));

12. Additional Coordinators

  • PaymentCoordinatorActor: Orchestrates multi-step payment flows including plugin-based transactions
  • BenfordCoordinatorActor: UTXO splitting using Benford's Law distribution for transaction privacy
  • TransactionLifecycleCoordinatorActor: Tracks pending transactions and recovers them on restart
  • ImportActor: Wallet import from blockchain via address discovery

Event Sourcing Flow

Commands → Events → State

// 1. Command represents user intention
final command = GenerateAddressCommand(
  commandId: 'gen-addr-1',
  walletId: 'wallet-001',
  purpose: AddressPurpose.receiving,
);

// 2. Command handler produces events
final events = [
  AddressGeneratedEvent(
    eventId: 'event-1',
    walletId: 'wallet-001',
    address: 'bc1q...',
    derivationPath: "m/44'/0'/0'/0/0",
    purpose: AddressPurpose.receiving,
    timestamp: DateTime.now(),
  ),
];

// 3. Events are applied to update state
final newState = currentState.applyEvent(events.first);

Event Types

All events are persisted to EventStore and streamed to Projections for read-model updates.

Wallet Events (BitcoinWalletAggregate)

  • WalletCreatedEvent: New wallet initialized
  • AddressGeneratedEvent: New address created
  • AddressLabelUpdatedEvent: Address label changed
  • UTXOReceivedEvent: Incoming UTXO detected
  • UTXOSpentEvent: UTXO consumed in transaction
  • UTXOConfirmationUpdatedEvent: UTXO confirmation count changed
  • TransactionAddedEvent: Transaction added to wallet
  • SpendingTransactionCreatedEvent: Outgoing transaction created
  • TransactionBroadcastEvent: Transaction sent to network

UTXO Reservation Events (BitcoinWalletAggregate)

  • UTXOReservationPlacedEvent: UTXO reserved for future use
  • UTXOReservationReleasedEvent: UTXO reservation removed
  • UTXOReservationExpiredEvent: UTXO reservation timed out
  • UTXOReservedEvent: UTXO marked as reserved
  • UTXOReleasedEvent: UTXO released from reservation
  • UTXOReservationRenewedEvent: UTXO reservation extended

Invoice Events (InvoiceAggregate)

  • InvoiceCreatedEvent: Invoice created with payment addresses
  • InvoiceStatusChangedEvent: Invoice status transition
  • InvoicePaidEvent: Invoice marked as paid after SPV validation
  • InvoiceExpiredEvent: Invoice expired before payment
  • InvoiceCancelledEvent: Invoice cancelled by user

Payment Channel Events (PaymentChannelAggregate)

  • ChannelOpenedEvent: Channel created with funding parameters
  • ChannelFundedEvent: Funding transaction broadcast
  • ChannelPaymentMadeEvent: Off-chain payment within channel
  • ChannelClosedEvent: Channel closed and settled on-chain

Note: All domain events are persisted to EventStore and streamed to their respective projections (WalletProjection, InvoiceProjection, ChannelProjection) for read model updates.

Security Features

SPV Verification

  • BEEF (Background Evaluation Extended Format): Validates transactions with parent transaction context
  • BUMP (BSV Universal Merkle Path): Efficient merkle proof format for SPV validation
  • Merkle Proof Validation: Cryptographic verification against block header chain
  • Header Chain Validation: Ensures block headers form a valid chain
  • Invoice-Based Address Verification: Confirms payments to expected addresses only
  • Transaction Authenticity: Verification without full blockchain download

UTXO Management

  • Atomic UTXO Selection: Reservation prevents double-spending
  • Automatic Cleanup: Expired reservations released automatically
  • Event-Sourced Tracking: Full history of UTXO lifecycle
  • Fee Calculation: Accurate fee computation from BEEF data

Payment Verification

  • Invoice System: Pre-allocated addresses for expected payments
  • Amount Validation: Confirms payment matches invoice amount
  • Expiration Handling: Time-limited invoices prevent indefinite address monitoring
  • Privacy: Single-use addresses linked to specific payments

Event Integrity

  • Immutable Event Log: All state changes permanently recorded
  • Complete Audit Trail: Full history of wallet operations
  • Snapshot Support: Performance optimization with integrity checks
  • Idempotent Commands: Safe command replay and retry

Configuration

Storage Backend

// Mobile/Desktop — Isar embedded database
final libspiffy = LibSpiffyActorSystem();
await libspiffy.initialize(dataDirectory: './wallet-data');

// Server — PostgreSQL
await libspiffy.initialize(
  storageBackend: StorageBackend.postgres,
  postgresConfig: PostgresConfig.fromConnectionString(
    'postgresql://user:pass@localhost:5432/wallets',
  ),
);

// Development — In-memory
await libspiffy.initialize(
  storageBackend: StorageBackend.inMemory,
);

Network Configuration

// ARC Service (for transaction broadcasting)
final arcConfig = ArcServiceConfig(
  baseUrl: 'https://arc.taal.com',
  apiKey: 'your-api-key',
  network: 'mainnet',
);
// Or use presets:
ArcServiceConfig.taalMainnet();
ArcServiceConfig.taalTestnet();

// CDN-based fast header sync
final cdnConfig = CdnHeaderSyncConfig(
  baseUrl: 'https://cdn.example.com/headers',
  concurrentDownloads: 4,
);

Monitoring and Observability

Actor Metrics

  • Message processing rates
  • Error rates and types
  • Actor lifecycle events
  • Memory usage and performance

Wallet Metrics

  • Balance changes over time
  • Transaction volume and fees
  • UTXO set size and distribution
  • Address generation patterns
  • Event store size and growth

Invoice Metrics

  • Invoice creation rate
  • Payment success rate
  • Invoice expiration rate
  • Average payment time
  • Active vs. paid vs. expired invoices

SPV Validation Metrics

  • Merkle proof validation success rate
  • BEEF/BUMP processing time
  • Fee calculation accuracy
  • Address verification success rate
  • Block header sync progress

Network Metrics

  • ARC service response times
  • Transaction broadcast success rate
  • Block header sync status
  • Network fee rates

Testing

# Run all tests
dart test

# Run by category
dart test test/unit/                    # Unit tests
dart test test/integration/             # Integration tests
dart test test/services/                # Service tests
dart test test/core_models/             # Domain model tests

Test Coverage (~67 test files)

  • Integration tests (~31): End-to-end flows including coordinator API, P2P payments, SPV validation, payment channels, token lifecycle, invoice persistence, wallet import, header sync
  • Unit tests (~8): Plugin registry, output specs, encryption, CDN sync, script builders
  • Service tests (~7): Transaction builder, block headers, SPV service, ARC service, payment channels
  • Core model tests (~5): UTXO, transaction, wallet state, commands, events
  • Storage tests (~3): Isar schemas, wallet storage, PostgreSQL integration
  • Actor/aggregate tests (~3): Header sync actor, channel aggregate, wallet aggregate
  • Format tests (~5): BEEF/BUMP parsing, format equivalence, SPV validation
  • Crypto tests (~2): DartSV crypto service, key derivation

Development

Project Structure

lib/
├── libspiffy.dart                       # Primary barrel file (~100 exports)
├── coordinator.dart                     # Public API (WalletCoordinatorActor)
└── src/
    ├── actors/                          # Actor System
    │   ├── libspiffy_actor_system.dart      # System initialization & event registration
    │   ├── wallet_coordinator_actor.dart     # Unified public API facade
    │   ├── wallet_manager_actor.dart         # Wallet aggregate coordinator
    │   ├── invoice_coordinator_actor.dart    # Invoice aggregate coordinator
    │   ├── payment_coordinator_actor.dart    # Payment flow orchestration
    │   ├── spv_actor.dart                    # SPV validation with BEEF/BUMP
    │   ├── arc_actor.dart                    # ARC service integration
    │   ├── header_sync_actor.dart            # Block header synchronization
    │   ├── benford_coordinator_actor.dart    # Privacy-preserving UTXO splitting
    │   ├── transaction_lifecycle_coordinator_actor.dart  # Pending tx recovery
    │   ├── import_actor.dart                 # Wallet import from blockchain
    │   ├── channel_p2p_adapter.dart          # Payment channel P2P communication
    │   ├── coordinator_messages.dart         # Public API commands/events
    │   ├── wallet_messages.dart              # Wallet actor messages
    │   ├── invoice_messages.dart             # Invoice actor messages
    │   ├── payment_messages.dart             # Payment flow messages
    │   ├── payment_channel_messages.dart     # Channel protocol messages
    │   └── spv_messages.dart                 # SPV validation messages
    ├── core/                            # Domain Aggregates (Write Side)
    │   ├── bitcoin_wallet_aggregate.dart     # Event-sourced wallet
    │   ├── invoice_aggregate.dart            # Event-sourced invoices
    │   ├── payment_channel_aggregate.dart    # Event-sourced payment channels
    │   ├── wallet_commands.dart              # Wallet command definitions
    │   ├── wallet_events.dart                # Wallet event definitions
    │   ├── invoice_commands.dart             # Invoice command definitions
    │   ├── invoice_events.dart               # Invoice event definitions
    │   ├── channel_commands.dart             # Channel command definitions
    │   ├── channel_events.dart               # Channel event definitions
    │   └── channel_state.dart                # Channel aggregate state
    ├── plugin/                          # Extensible Plugin System
    │   ├── script_plugin.dart               # Base plugin interface
    │   ├── transaction_builder_plugin.dart   # Multi-output transaction builder
    │   ├── plugin_registry.dart             # Plugin discovery & management
    │   └── plugin_types.dart                # Plugin data structures
    ├── projections/                     # CQRS Read Side
    │   ├── wallet_projection.dart           # Wallet read model updates
    │   ├── invoice_projection.dart          # Invoice read model updates
    │   └── channel_projection.dart          # Channel read model updates
    ├── models/                          # Domain Models
    │   ├── wallet_state.dart                # Wallet aggregate state
    │   ├── wallet_read_model.dart           # Wallet query model
    │   ├── invoice_state.dart               # Invoice aggregate state
    │   ├── invoice_read_model.dart          # Invoice query model
    │   ├── invoice_output_spec.dart         # Multi-output specs (P2PKH, P2MS, OP_RETURN, Plugin)
    │   ├── bitcoin_utxo.dart                # UTXO model with plugin metadata
    │   ├── bitcoin_transaction.dart         # Transaction model
    │   ├── address_metadata.dart            # Address with script type and usage
    │   ├── blockchain_data_models.dart      # Blockchain API response models
    │   ├── payment_channel.dart             # Channel read model
    │   ├── transaction_address_link.dart    # Transaction-address junction
    │   └── wallet_type.dart                 # Enum: HD, WIF, XPRIV, XPUB
    ├── spv/                             # SPV Validation
    │   ├── beef.dart                        # BEEF format implementation
    │   ├── bump.dart                        # BUMP merkle path implementation
    │   ├── block_header_chain.dart          # Header chain management
    │   ├── cdn_header_sync_service.dart     # Fast CDN-based header sync
    │   ├── cdn_header_sync_config.dart      # CDN sync configuration
    │   └── cdn_manifest.dart                # CDN manifest structures
    ├── storage/                         # Persistence Layer
    │   ├── read_model_storage.dart          # Read model interface
    │   ├── event_storage.dart               # Event store interface
    │   ├── secure_storage.dart              # Encrypted key storage interface
    │   ├── storage_backend.dart             # Backend enum & factory
    │   ├── isar_wallet_storage.dart         # Isar implementation (mobile/desktop)
    │   ├── in_memory_wallet_storage.dart    # In-memory (dev/test)
    │   ├── in_memory_secure_storage.dart    # In-memory key storage (dev/test)
    │   ├── libspiffy_schemas.dart           # Isar schema definitions
    │   ├── payment_channel_entity.dart      # Channel Isar entity
    │   └── postgres/                        # PostgreSQL backend (server)
    │       ├── postgres_config.dart             # Connection & pool config
    │       ├── postgres_wallet_storage.dart      # Read model store
    │       ├── postgres_event_store.dart         # Event sourcing store
    │       ├── postgres_secure_storage.dart      # Encrypted key storage
    │       ├── postgres_migrations.dart          # Migration infrastructure
    │       └── migrations/                      # Schema versions
    │           ├── v001_initial_schema.dart
    │           └── v002_secure_secrets.dart
    ├── services/                        # Business Logic Services
    │   ├── crypto_service.dart              # Cryptographic interface (BIP32/39/44)
    │   ├── dartsv_crypto_service.dart       # DartSV crypto implementation
    │   ├── callback_transaction_signer.dart # Secure signer for plugins
    │   ├── arc_service.dart                 # ARC API client
    │   ├── arc_service_config.dart          # ARC configuration
    │   ├── spv_service.dart                 # SPV validation logic
    │   ├── block_header_service.dart        # Header management & reorgs
    │   ├── wallet_balance_service.dart      # BEEF-based balance tracking
    │   ├── ancestor_chain_service.dart      # Transaction ancestry chains
    │   ├── transaction_builder_service.dart # Transaction construction
    │   ├── payment_channel_builder.dart     # Channel transaction builder
    │   ├── address_discovery_service.dart   # Hierarchical address discovery
    │   ├── script_type_registry.dart        # Script type identification
    │   ├── transaction_analyzer.dart        # Two-phase UTXO analysis
    │   ├── transaction_import_service.dart  # Historical transaction import
    │   ├── blockchain_data_source.dart      # Blockchain API interface
    │   ├── whatsonchain_data_source.dart    # WhatsOnChain implementation
    │   └── transaction/builder/             # Lock/unlock script builders
    │       ├── p2pkh_lockbuilder.dart           # Standard P2PKH
    │       ├── p2pkh_unlockbuilder.dart
    │       ├── hodl_lockbuilder.dart            # Time-locked scripts
    │       ├── hodl_unlockbuilder.dart
    │       ├── op_return_lockbuilder.dart       # OP_RETURN metadata
    │       └── ...                              # AIP, BMAP, B://, PP1, PP2
    ├── crypto/                          # Encryption
    │   └── encryption_service.dart          # AES-256-GCM with HKDF
    ├── integration/                     # External System Bridges
    │   └── spiffynode_bridge.dart           # SpiffyNode P2P bridge
    └── utils/                           # Utilities
        ├── beef.dart                        # BEEF format parsing
        ├── bump.dart                        # BUMP merkle path utilities
        ├── benford_distribution.dart        # Benford's Law splitting
        ├── crypto_utils.dart                # Cryptographic helpers
        ├── hex_utils.dart                   # Hex conversion
        └── tsc_converter.dart               # Token/satoshi conversion

Key Architectural Layers:

  • actors/: Long-lived coordinators that route commands; WalletCoordinatorActor is the single public entry point
  • core/: Event-sourced aggregates (write-side domain logic)
  • plugin/: Extensible system for custom script types and token protocols
  • projections/: Read-side event handlers (update read models)
  • models/: Separated into aggregate state (mutable) and read models (denormalized)
  • spv/: BEEF/BUMP validation and block header synchronization
  • storage/: Read model persistence — Isar (mobile), PostgreSQL (server), in-memory (dev); EventStore managed by Eventador

Adding New Features

Adding Wallet Commands/Events (CQRS Pattern)

Follow these steps to add new functionality using proper CQRS patterns:

Step 1: Define Command

// Add to lib/src/core/wallet_commands.dart
class MyNewCommand extends Command {
  final String walletId;
  final String someParameter;
  
  MyNewCommand({
    required this.walletId,
    required this.someParameter,
  });
}

Step 2: Define Event

// Add to lib/src/core/wallet_events.dart
class MyNewEvent extends WalletEvent {
  final String someData;
  
  MyNewEvent({
    required String walletId,
    required this.someData,
    String? eventId,
    DateTime? timestamp,
    int? version,
  }) : super(
    walletId: walletId,
    eventId: eventId,
    timestamp: timestamp,
    version: version,
  );
  
  @override
  Map<String, dynamic> getEventData() {
    return {
      'someData': someData,
    };
  }
  
  // CRITICAL: fromMap for deserialization after restart
  static MyNewEvent fromMap(Map<String, dynamic> map) {
    return MyNewEvent(
      walletId: map['walletId'] as String,
      someData: map['someData'] as String,
      eventId: map['eventId'] as String?,
      timestamp: map['timestamp'] != null
          ? (map['timestamp'] is String
              ? DateTime.parse(map['timestamp'])
              : map['timestamp'] as DateTime)
          : null,
      version: map['version'] as int?,
    );
  }
}

Step 3: Register Event Type

// Add to lib/src/actors/libspiffy_actor_system.dart → _registerEventTypes()
EventRegistry.register<MyNewEvent>(
  'MyNewEvent',
  (map) => MyNewEvent.fromMap(map),
);

Step 4: Add Command Handler in BitcoinWalletAggregate

// In lib/src/core/bitcoin_wallet_aggregate.dart → handleCommand()
@override
Future<List<Event>> handleCommand(WalletState currentState, Command command) async {
  return switch (command.runtimeType) {
    MyNewCommand => _handleMyNewCommand(currentState, command as MyNewCommand),
    // ... other commands
    _ => throw ArgumentError('Unknown command: ${command.runtimeType}'),
  };
}

List<Event> _handleMyNewCommand(WalletState state, MyNewCommand cmd) {
  // Validate business rules
  if (!state.isCreated) {
    throw StateError('Wallet not yet created');
  }
  
  // Perform business logic
  final result = performSomeOperation(cmd.someParameter);
  
  // Return events (not state changes!)
  return [
    MyNewEvent(
      walletId: cmd.walletId,
      someData: result,
    ),
  ];
}

Step 5: Add Event Handler in BitcoinWalletAggregate

// In lib/src/core/bitcoin_wallet_aggregate.dart → eventHandler()
@override
void eventHandler(Event event) {
  ensureStateInitialized(); // CRITICAL!
  
  if (event is! WalletEvent) {
    throw ArgumentError('Expected WalletEvent, got ${event.runtimeType}');
  }

  switch (event.runtimeType) {
    case MyNewEvent:
      _applyMyNewEvent(event as MyNewEvent);
      break;
    // ... other events
    default:
      throw ArgumentError('Unknown event: ${event.runtimeType}');
  }
}

// Mutate currentState directly (new Eventador pattern)
void _applyMyNewEvent(MyNewEvent event) {
  currentState.someField = event.someData;
  currentState.version++;
  currentState.lastModified = event.timestamp;
}

Step 6: Update Projection (if needed)

// In lib/src/projections/wallet_projection.dart → handle()
@override
Future<bool> handle(Event event) async {
  if (event is MyNewEvent) {
    // Update read model in Isar
    await _storage.updateSomeReadModel(
      event.walletId,
      event.someData,
    );
    return true;
  }
  // ... other events
}

Key Points:

  • ✅ Commands go to aggregates via coordinators
  • ✅ Aggregates validate and emit events
  • ✅ Events are persisted to EventStore automatically
  • ✅ Projections update read models asynchronously
  • ✅ All event types MUST be registered for deserialization
  • ❌ Never write to storage from aggregates or coordinators

Adding Actor Messages

  1. Define Message in wallet_messages.dart or custom file

    class MyNewMessage implements Message {
      final String data;
      
      MyNewMessage(this.data);
      
      @override
      String? get correlationId => null;
      @override
      Map<String, dynamic> get metadata => {'data': data};
    }
  2. Handle Message in Actor

    @override
    Future<void> onMessage(dynamic message) async {
      switch (message.runtimeType) {
        case MyNewMessage:
          await _handleMyNewMessage(message as MyNewMessage);
          break;
        // ... other cases
      }
    }
    
    Future<void> _handleMyNewMessage(MyNewMessage msg) async {
      // Process message
      // Optionally send response
      context.sender?.tell(MyResponseMessage(...));
    }

Best Practices

Event Sourcing Patterns

DO:

  • Keep events immutable and descriptive
  • Store business intent in events, not just data changes
  • Use event versioning for schema evolution
  • Apply events in order to rebuild state

DON'T:

  • Query the EventStore directly for business logic
  • Modify events after they're persisted
  • Store computed values in events (recalculate from state)
  • Skip event application during replay

Actor Communication

DO:

  • Use message-passing for all actor communication
  • Implement proper command-response patterns
  • Handle timeouts and failures gracefully
  • Keep messages immutable

DON'T:

  • Access actors' internal state directly
  • Use fire-and-forget for operations requiring confirmation
  • Block waiting for responses (use async patterns)
  • Share mutable state between actors

Actor System Integration

DO:

  • Use integrated mode when building actor-based applications
  • Let the host application manage actor system lifecycle
  • Use standalone mode for simple use cases or microservices
  • Check ownsActorSystem if lifecycle management is unclear
  • Provide custom storage/crypto implementations via initialization

DON'T:

  • Create multiple actor systems unnecessarily
  • Shutdown the host's actor system from LibSpiffy
  • Mix standalone and integrated modes in same application
  • Assume LibSpiffy owns the actor system without checking

SPV Validation

DO:

  • Always validate merkle proofs against block headers
  • Verify payment amounts match invoices
  • Calculate fees from BEEF data
  • Use invoice-based address verification

DON'T:

  • Trust transaction data without merkle proof
  • Accept payments to unexpected addresses
  • Skip block header chain validation
  • Process transactions without proper BEEF context

UTXO Management

DO:

  • Reserve UTXOs before transaction creation
  • Set expiration times on reservations
  • Release reservations after transaction broadcast
  • Track UTXO lifecycle through events

DON'T:

  • Spend UTXOs without reservation
  • Keep indefinite reservations
  • Manually track UTXO state outside events
  • Modify UTXO state without commands/events

Documentation

Acknowledgments

  • Dactor: Actor model framework for Dart
  • Eventador: Event sourcing and CQRS library
  • DuraQ: Operational workflow management
  • DartSV: Bitcoin SV library for Dart
  • SpiffyNode: SPV chain tracking and P2P connectivity

Further Reading

Architecture Patterns

Bitcoin & BSV

License

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

About

An actor-based Bitcoin SPV Wallet Library

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages