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 LibSpiffyvoidregisterMyCustomEvents() {
EventRegistry.register<MyCustomWalletEvent>(
'MyCustomWalletEvent',
(map) =>MyCustomWalletEvent.fromMap(map),
);
}
// Then initialize LibSpiffyawaitinitializeLibSpiffy(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 LibSpiffyfinal libspiffy =LibSpiffyActorSystem();
await libspiffy.initialize(
dataDirectory:'./wallet-data',
arcConfig:ArcServiceConfig.taalMainnet(),
enableP2P:true,
);
// Use the coordinator - THE single entry pointfinal coordinator = libspiffy.coordinator;
// Subscribe to events
libspiffy.coordinatorEvents?.listen((event) {
if (event isWalletCreatedEvent) {
print('Wallet created: ${event.walletId}');
} elseif (event isPaymentReadyEvent) {
print('BEEF ready to send: ${event.txid}');
} elseif (event isBalanceResponse) {
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'));
// Cleanupawait libspiffy.shutdown();For advanced use cases, you can access internal actors directly:
import'package:libspiffy/libspiffy.dart';
awaitinitializeLibSpiffy(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'));
awaitshutdownLibSpiffy();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 systemfinal hostActorSystem =LocalActorSystem(ActorSystemConfig());
// Initialize LibSpiffy using your actor systemawaitinitializeLibSpiffy(
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 LibSpiffyfinal myActor =await hostActorSystem.spawn(
'payment-processor',
() =>PaymentProcessorActor(
walletManager:getLibSpiffySystem().walletManager,
invoiceCoordinator:getLibSpiffySystem().invoiceCoordinator,
),
);
// When shutting down, LibSpiffy won't shutdown the host's actor systemawaitshutdownLibSpiffy(); // Only closes LibSpiffy's resources// Host manages its own actor systemawait 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 everythingawaitinitializeLibSpiffy(dataDirectory:'./data');
// Use wallet functionalityfinal walletManager =getLibSpiffySystem().walletManager;
walletManager.tell(CreateWalletMessage(...));
// LibSpiffy handles its own lifecycleawaitshutdownLibSpiffy();Best for: Simple applications, microservices, or when LibSpiffy is the primary component.
Integrate LibSpiffy into an existing actor-based application:
// Host application setupfinal app =MyActorBasedApp();
await app.initialize();
// LibSpiffy joins the host's actor systemawaitinitializeLibSpiffy(
actorSystem: app.actorSystem,
dataDirectory:'./data',
);
// Your actors can directly communicate with LibSpiffy actorsfinal paymentProcessor =await app.actorSystem.spawn(
'payment-processor',
() =>PaymentProcessorActor(
walletManager:getLibSpiffySystem().walletManager,
spvActor:getLibSpiffySystem().spvActor,
),
);
// Lifecycle managementawaitshutdownLibSpiffy(); // Closes LibSpiffy resources onlyawait 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 neededfinal 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 eventsgetLibSpiffySystem().coordinatorEvents?.listen((event) {
switch (event) {
caseWalletCreatedEvent e:handleWalletCreated(e);
casePaymentReadyEvent e:handlePaymentReady(e);
caseBEEFValidationResultEvent e:handleBEEFValidated(e);
caseErrorEvent 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 systemif (getLibSpiffySystem().ownsActorSystem) {
print('LibSpiffy is running in standalone mode');
} else {
print('LibSpiffy is integrated with host actor system');
}
// Access the underlying actor system if neededfinal 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 actorfinal 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()@overrideFuture<List<Event>> handleCommand(InvoiceState currentState, Command command) async {
if (command isCreateInvoiceCommand) {
// Validate business rulesif (command.amount <=BigInt.zero) {
throwArgumentError('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()@overridevoideventHandler(Event event) {
ensureStateInitialized(); // Critical for recoveryif (event isInvoiceCreatedEvent) {
// 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 projectionsawait projectionManager.start(); // Starts event stream processingStep 7: Projection Handling
// InvoiceProjection.handle() receives events@overrideFuture<bool> handle(Event event) async {
if (event isInvoiceCreatedEvent) {
// Create denormalized read modelfinal 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 replayawaitupdateCheckpoint(event.version);
returntrue; // Event handled
}
returnfalse; // Event not handled by this projection
}Step 8: Query Execution
// Queries NEVER touch EventStore, only ReadModelStorage
invoiceCoordinator.tell(CheckInvoiceMessage(invoiceId));
// Inside coordinatorFuture<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 recoveryfinal 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 addressesfinal invoice =awaitcreateInvoice(
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 =awaitcreateTransaction(
fromWallet:'alice-wallet',
toAddresses: [invoice.addresses.first],
amount: invoice.amount,
);
// 3. Sender broadcasts transaction with BEEF (includes merkle proof)awaitbroadcastTransaction(
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 actorclassBitcoinWalletAggregateextendsAggregateRoot<WalletState> {
@overrideFuture<List<Event>> handleCommand(WalletState currentState, Command command) async {
// Validate business rules and return eventsif (command isGenerateAddressCommand) {
return [AddressGeneratedEvent(
walletId: persistenceId,
address: generatedAddress,
derivationIndex: currentState.nextDerivationIndex,
// ...
)];
}
// ... other command handlers
}
@overridevoideventHandler(Event event) {
ensureStateInitialized(); // Critical!// Mutate currentState directly based on eventsif (event isAddressGeneratedEvent) {
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 invoiceclassInvoiceAggregateextendsAggregateRoot<InvoiceState> {
@overrideFuture<List<Event>> handleCommand(InvoiceState currentState, Command command) async {
if (command isCreateInvoiceCommand) {
return [InvoiceCreatedEvent(
invoiceId: persistenceId,
walletId: command.walletId,
addresses: command.addresses,
amount: command.amount,
createdAt:DateTime.now(),
// ...
)];
}
if (command isMarkInvoicePaidCommand) {
// Business rule validationif (currentState.status !=InvoiceStatus.pending) {
throwStateError('Invoice is not pending');
}
return [InvoicePaidEvent(
invoiceId: persistenceId,
paidAt:DateTime.now(),
txid: command.txid,
amountReceived: command.amountReceived,
)];
}
// ... other commands
}
@overridevoideventHandler(Event event) {
ensureStateInitialized();
if (event isInvoiceCreatedEvent) {
currentState.status =InvoiceStatus.pending;
currentState.amount = event.amount;
currentState.addresses = event.addresses;
// ...
}
if (event isInvoicePaidEvent) {
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 modelsclassWalletProjectionextendsProjection<WalletReadModel> {
@overrideFuture<bool> handle(Event event) async {
if (event isUTXOReceivedEvent) {
// Update denormalized UTXO view in Isarawait storage.storeUTXO(BitcoinUtxo.fromEvent(event));
returntrue;
}
// ... other wallet events
}
}
// InvoiceProjection - Updates invoice read models classInvoiceProjectionextendsProjection<InvoiceReadModel> {
@overrideFuture<bool> handle(Event event) async {
if (event isInvoiceCreatedEvent) {
// Check for existing invoice (idempotent replay)final existing =await storage.getInvoice(event.invoiceId);
if (existing ==null) {
await storage.storeInvoice(Invoice.fromEvent(event));
}
returntrue;
}
if (event isInvoicePaidEvent) {
// Update existing invoice statusawait storage.updateInvoiceStatus(
event.invoiceId,
InvoiceStatus.paid,
paidAt: event.paidAt,
txid: event.txid,
);
returntrue;
}
// ... 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 LibSpiffyActorSystemfinal projectionManager =ProjectionManager(eventStore);
// Register projectionsawait projectionManager.registerProjection(walletProjection);
await projectionManager.registerProjection(invoiceProjection);
// Start event streamingawait 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 intentionfinal command =GenerateAddressCommand(
commandId:'gen-addr-1',
walletId:'wallet-001',
purpose:AddressPurpose.receiving,
);
// 2. Command handler produces eventsfinal 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 statefinal 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 databasefinal libspiffy =LibSpiffyActorSystem();
await libspiffy.initialize(dataDirectory:'./wallet-data');
// Server — PostgreSQLawait libspiffy.initialize(
storageBackend:StorageBackend.postgres,
postgresConfig:PostgresConfig.fromConnectionString(
'postgresql://user:pass@localhost:5432/wallets',
),
);
// Development — In-memoryawait 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 syncfinal 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.dartclassMyNewCommandextendsCommand {
finalString walletId;
finalString someParameter;
MyNewCommand({
requiredthis.walletId,
requiredthis.someParameter,
});
}Step 2: Define Event
// Add to lib/src/core/wallet_events.dartclassMyNewEventextendsWalletEvent {
finalString someData;
MyNewEvent({
requiredString walletId,
requiredthis.someData,
String? eventId,
DateTime? timestamp,
int? version,
}) :super(
walletId: walletId,
eventId: eventId,
timestamp: timestamp,
version: version,
);
@overrideMap<String, dynamic> getEventData() {
return {
'someData': someData,
};
}
// CRITICAL: fromMap for deserialization after restartstaticMyNewEventfromMap(Map<String, dynamic> map) {
returnMyNewEvent(
walletId: map['walletId'] asString,
someData: map['someData'] asString,
eventId: map['eventId'] asString?,
timestamp: map['timestamp'] !=null? (map['timestamp'] isString?DateTime.parse(map['timestamp'])
: map['timestamp'] asDateTime)
:null,
version: map['version'] asint?,
);
}
}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()@overrideFuture<List<Event>> handleCommand(WalletState currentState, Command command) async {
returnswitch (command.runtimeType) {
MyNewCommand=>_handleMyNewCommand(currentState, command asMyNewCommand),
// ... other commands
_ =>throwArgumentError('Unknown command: ${command.runtimeType}'),
};
}
List<Event> _handleMyNewCommand(WalletState state, MyNewCommand cmd) {
// Validate business rulesif (!state.isCreated) {
throwStateError('Wallet not yet created');
}
// Perform business logicfinal 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()@overridevoideventHandler(Event event) {
ensureStateInitialized(); // CRITICAL!if (event is!WalletEvent) {
throwArgumentError('Expected WalletEvent, got ${event.runtimeType}');
}
switch (event.runtimeType) {
caseMyNewEvent:_applyMyNewEvent(event asMyNewEvent);
break;
// ... other eventsdefault:throwArgumentError('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()@overrideFuture<bool> handle(Event event) async {
if (event isMyNewEvent) {
// Update read model in Isarawait _storage.updateSomeReadModel(
event.walletId,
event.someData,
);
returntrue;
}
// ... 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
classMyNewMessageimplementsMessage { finalString data; MyNewMessage(this.data); @overrideString?get correlationId =>null; @overrideMap<String, dynamic> get metadata => {'data': data}; }
Handle Message in Actor
@overrideFuture<void> onMessage(dynamic message) async { switch (message.runtimeType) { caseMyNewMessage:await_handleMyNewMessage(message asMyNewMessage); 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.