An actor-based Bitcoin wallet implementation using event sourcing, CQRS, and SPV (Simplified Payment Verification) built with the Dactor/Eventador/DuraQ stack.
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)
- 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
- 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
- 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
- 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
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. │ │
│ └──────────────────┘ └────────────────┘ └─────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────────┘
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
- Dart SDK 3.5.1 or later
- Dependencies: dactor, eventador, duraq, dartsv
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.
# Clone the repository
git clone <repository-url>
cd libspiffy
# Install dependencies
dart pub get
# Run the example
dart run example/bitcoin_wallet_example.dartThe 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();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();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();- 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
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.
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 shutdownBest for: Complex applications with multiple actor-based subsystems.
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.
// 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;LibSpiffy implements a complete CQRS (Command Query Responsibility Segregation) architecture with event sourcing. Understanding this flow is crucial for working with the system.
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 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 calledStep 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++;
}
}Step 6: Event Streaming
// ProjectionManager subscribes to EventStore
// Streams events to registered projections
await projectionManager.start(); // Starts event stream processingStep 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
));
}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✅ 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
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
LibSpiffy implements a streamlined SPV payment verification system using 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- 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
- 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
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
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
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
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!)
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)
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 gracefullyHandles 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 paidInterfaces 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());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',
));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.
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'));- 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
// 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);All events are persisted to EventStore and streamed to Projections for read-model updates.
- 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
- 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
- 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
- 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.
- 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
- 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
- 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
- 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
// 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,
);// 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,
);- Message processing rates
- Error rates and types
- Actor lifecycle events
- Memory usage and performance
- Balance changes over time
- Transaction volume and fees
- UTXO set size and distribution
- Address generation patterns
- Event store size and growth
- Invoice creation rate
- Payment success rate
- Invoice expiration rate
- Average payment time
- Active vs. paid vs. expired invoices
- Merkle proof validation success rate
- BEEF/BUMP processing time
- Fee calculation accuracy
- Address verification success rate
- Block header sync progress
- ARC service response times
- Transaction broadcast success rate
- Block header sync status
- Network fee rates
# 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- 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
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
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
-
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(...)); }
✅ 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
✅ 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
✅ 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
✅ 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
✅ 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
- 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
- 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
- SPV (Simplified Payment Verification)
- BEEF Specification - Background Evaluation Extended Format
- BUMP Specification - BSV Universal Merkle Path
- BRC-71 Standard - Merkle Path Format
This project is licensed under the MIT License - see the LICENSE file for details.