libspiffy 1.1.0
libspiffy: ^1.1.0 copied to clipboard
An actor-based Bitcoin SPV Wallet Library
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 #
- Transaction Received: SPV Actor receives transaction with BEEF and invoice ID
- Merkle Proof Validation: Validates transaction is in a valid block
- Invoice Lookup: Retrieves expected payment addresses from Invoice Manager
- Output Verification: Confirms transaction pays to invoice addresses
- Amount Validation: Verifies payment amount matches invoice
- UTXO Extraction: Identifies new spendable UTXOs and spent UTXOs
- Fee Calculation: Computes transaction fee from input/output values in BEEF
- State Update: Wallet state updated via event sourcing
- 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
-
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}; } -
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
ownsActorSystemif 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 #
- Developer Guide — Public API reference and programming model
- Plugin API Guide — Building custom script/token plugins
- Multi-Output Invoice Guide — P2PKH, P2MS, OP_RETURN, and plugin outputs
- CDN Header Sync Guide — Fast block header synchronization
- PostgreSQL Secure Storage Guide — Server deployment with encrypted keys
- Projections Guide — Building CQRS read models
- Wallet Architecture — Detailed system architecture
- SPV Understanding — SPV concepts and implementation
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 #
- SPV (Simplified Payment Verification)
- BEEF Specification - Background Evaluation Extended Format
- BUMP Specification - BSV Universal Merkle Path
- BRC-71 Standard - Merkle Path Format
License #
This project is licensed under the MIT License - see the LICENSE file for details.