Quick start
This page gets libspiffy running in one Dart file: you start the actor system, create a wallet, create an invoice, query the balance, handle a failure and shut down. For a full payment between two wallets, follow the tutorial.
How you talk to libspiffy
Section titled “How you talk to libspiffy”libspiffy.coordinator is a WalletCoordinator. It has three methods:
| Method | What it does |
|---|---|
ask(request, {timeout}) |
Sends a command or query and returns a Future of its own reply. |
tell(command) |
Sends without waiting. A reply, if there is one, arrives only on the event stream. |
on<E>({walletId}) |
A Stream<E> of one kind of event, of one wallet when you name it. Use it for what happens without a request: a balance change, an invoice paid, a channel request from a peer. |
Every command or query with an answer is a CoordinatorRequest<R>. It names its reply type R (for
CreateWalletCommand, a WalletCreatedEvent), and its requestId is fixed when you create it: generated,
unless you pass one. The coordinator answers each request with exactly one R carrying that requestId,
so two requests of the same kind running at once each get their own reply.
// Sketch: the shape of every interaction.final balance = await libspiffy.coordinator.ask(GetBalanceQuery(walletId: 'shop'));print(balance.totalBalance);The complete program
Section titled “The complete program”This program runs against testnet settings with P2P off, so it needs no network to finish. It writes its event store to a fresh temporary directory, so you can run it again.
import 'dart:io';
import 'package:libspiffy/coordinator.dart';import 'package:libspiffy/libspiffy.dart';
Future<void> main() async { final libspiffy = LibSpiffyActorSystem(); await libspiffy.initialize( dataDirectory: Directory.systemTemp.createTempSync('quick_start_').path, networkType: 'test', arcConfig: ArcServiceConfig.taalTestnet(), enableP2P: false, ); final coordinator = libspiffy.coordinator;
// Events nobody asked for: a balance change, a failure no request caused. final balances = coordinator .on<BalanceUpdatedEvent>(walletId: 'shop') .listen((e) => print('shop balance is now ${e.totalBalance} sats')); final errors = coordinator .on<ErrorEvent>() .listen((e) => print('error from ${e.source}: ${e.message}'));
// 1. Create an HD wallet. The app supplies the mnemonic and must keep it. final mnemonic = await libspiffy.cryptoService.generateMnemonic(); final wallet = await coordinator.ask(CreateWalletCommand( walletId: 'shop', name: 'Shop wallet', mnemonic: mnemonic, )); print('Root address: ${wallet.rootAddress}');
// 2. Create an invoice for 10,000 sats. final invoice = await coordinator.ask(CreateInvoiceCommand( walletId: 'shop', amount: BigInt.from(10000), description: 'Order 1001', expiresIn: const Duration(hours: 1), )); print('Invoice ${invoice.invoiceId}: pay ${invoice.amount} sats ' 'to ${invoice.addresses.first}');
// 3. Read the balance. final balance = await coordinator.ask(GetBalanceQuery(walletId: 'shop')); print('Spendable: ${balance.totalBalance} sats ' '(${balance.confirmedBalance} confirmed, ' '${balance.unconfirmedBalance} unconfirmed)');
// 4. A request that fails throws CoordinatorFailure. try { await coordinator.ask(CreateWalletCommand( walletId: 'shop', // already exists name: 'Shop wallet', mnemonic: mnemonic, )); } on CoordinatorFailure catch (failure) { print('Not created: ${failure.message}'); // "Wallet already exists" }
// 5. Shut down: stops the actors and closes the stores libspiffy opened. await balances.cancel(); await errors.cancel(); await libspiffy.shutdown();}A reply that ask returns is always a success: you do not check success on it. WalletCreatedEvent
arrives once the read model holds the wallet and its rootAddress, and InvoiceCreatedEvent once it holds
the invoice, so a query you send next sees them.
Handling failures
Section titled “Handling failures”ask throws CoordinatorFailure when the request failed. It has three fields and one getter:
| Member | What it holds |
|---|---|
requestId |
The failed request’s requestId. |
message |
Why it failed. |
event |
What reported the failure: the request’s own reply (for example a WalletCreatedEvent with success: false), or an ErrorEvent that names the request. Null when the coordinator stopped first. |
closed |
True when the coordinator stopped (shutdown()) before it answered. |
// Sketch: tell a refusal from a shutdown.try { await coordinator.ask(DeleteWalletCommand(walletId: 'old-wallet'));} on CoordinatorFailure catch (failure) { if (failure.closed) return; // libspiffy is shutting down showError(failure.message); // showError is your own UI code}Failures that no request caused, such as a broadcast retried later or a channel step the counterparty
started, arrive as an ErrorEvent with a null requestId. Follow them with coordinator.on<ErrorEvent>(),
as the program above does. source names the operation that failed and walletId, when set, the wallet.
Timeouts
Section titled “Timeouts”ask throws TimeoutException (from dart:async) when no reply arrives in time. Each request has its own
default, its replyTimeout: one minute for most requests, more for those that wait on the network
(CoordinatorRequest.paymentTimeout is 3 minutes, receiveTimeout 5, importTimeout an hour). Pass
timeout: to choose your own.
// Sketch: wait at most 10 seconds for the balance.try { final balance = await coordinator.ask( GetBalanceQuery(walletId: 'shop'), timeout: const Duration(seconds: 10), ); render(balance); // render is your own UI code} on TimeoutException { showStale(); // the request may still finish}Following events with on()
Section titled “Following events with on()”on<E>() filters the coordinator’s event stream by type, and by walletId when you pass one. Replies are
published there too, so on<InvoiceCreatedEvent>() sees every invoice created, whoever asked.
libspiffy.coordinatorEvents is the whole stream, if you want every event in one listener.
// Sketch: what an app usually follows.coordinator.on<InvoicePaidEvent>(walletId: 'shop').listen(markOrderPaid);coordinator.on<BalanceUpdatedEvent>(walletId: 'shop').listen(showBalance);coordinator.on<ErrorEvent>().listen(logError);A reply type can also arrive with no request behind it, for example a payment replayed when its block
header arrives. Its requestId is null then.
initialize() parameters
Section titled “initialize() parameters”LibSpiffyActorSystem.initialize() takes only named parameters. The ones you use first:
| Parameter | Default | What it does |
|---|---|---|
networkType |
'test' |
'main', 'test' or 'regtest' ('mainnet', 'livenet' and 'testnet' are accepted too). Selects the genesis header, DNS seeds and address encoding. |
arcConfig |
ArcServiceConfig.gorillaPoolMainnet() on mainnet, otherwise ArcServiceConfig.taalTestnet() |
The ARC or Arcade endpoint for broadcasts and fee rates. Presets: taalMainnet, taalTestnet, gorillaPoolMainnet, gorillaPoolTestnet, bsvaArcadeTestnet, or ArcServiceConfig.custom(baseUrl: ...). |
enableP2P |
true |
Connect to BSV peers and sync block headers. |
peerAddresses |
the network’s DNS seeds | Peers as 'host:port'. Regtest has no seeds, so name your peers there. |
userAgent |
'/LibSpiffy-BSV:1.0/' |
Sent in the version handshake. Teranode bans peers whose user agent lacks BSV or Bitcoin SV. |
startHeight |
none | The height reported to peers in the handshake. Header sync does not start from it; it continues from the stored chain’s tip. |
isar |
none | An Isar instance your app opened with LibSpiffySchemas.allSchemas. libspiffy stores events, read models, headers and the broadcast retry queue in it. |
dataDirectory |
'./data' when needed |
Used only when you pass no isar: libspiffy opens its own Isar event store here. |
secureStorage |
InMemorySecureStorage() |
Where mnemonics, WIFs and xprivs live. The default loses them on restart. |
storageBackend |
StorageBackend.isar |
isar, postgres (with postgresConfig) or inMemory. |
blockchainDataSource |
none | Where ImportWalletCommand fetches a wallet’s history, for example a WhatsOnChainDataSource. Without one, an import fails. |
cdnBaseUrl |
none | A header CDN to sync from before peers. See header sync via CDN. |
The full list, including payment channel and deferred payment settings, is in the API docs.
When libspiffy is ready
Section titled “When libspiffy is ready”initialize() returns after the actors start, the optional CDN header sync finishes, and the P2P layer has
connected to at least one peer. On a first install with a header CDN that can take minutes.
You do not have to wait that long to send commands. libspiffy.ready is a Future that completes as soon
as the actors take commands, before header sync; libspiffy.isReady tells you whether it has. Creating or
importing a wallet needs no headers, and a received payment whose block header has not arrived waits for
it.
// Sketch: start in the background, act as soon as the actors run.final started = libspiffy.initialize(networkType: 'test', isar: isar);await libspiffy.ready;final wallet = await libspiffy.coordinator.ask( CreateWalletCommand(walletId: 'shop', name: 'Shop', mnemonic: mnemonic));await started; // header sync and P2P are upIf enableP2P is true and no peer can be reached, initialize() throws a StateError (“P2P
initialization failed …”) after the actors have started. The system stays usable without P2P; catch the
error if your app can live with that.
Using tell() and the event stream
Section titled “Using tell() and the event stream”ask is what most code needs. If you would rather send and move on, tell the request and find its reply
on the stream by requestId. Pass your own requestId, or read the generated one from the request:
// Sketch: fire and forget, then match the reply yourself.final query = GetBalanceQuery(walletId: 'shop');coordinator .on<BalanceResponse>(walletId: 'shop') .where((r) => r.requestId == query.requestId) .first .then((r) => print('balance ${r.totalBalance}'));coordinator.tell(query);With tell, a failure is on the reply itself: success and error on most replies, and
CoordinatorReply.failure gives the reason for any reply type. An ErrorEvent that a request caused
carries the request’s requestId. Subscribe before you tell, or the reply can pass before you listen.
Shutting down
Section titled “Shutting down”await libspiffy.shutdown() disconnects from peers, stops the projections and actors, and closes what
libspiffy opened. A request still waiting in ask then fails with a CoordinatorFailure whose closed is
true. shutdown() does not shut down an actor system or close an Isar instance that your app passed in. A
shut-down LibSpiffyActorSystem cannot be initialized again; create a new instance.
Upgrading from 4.x
Section titled “Upgrading from 4.x”libspiffy 5.0.0 changes how you talk to the coordinator. The breaking changes an app usually hits:
libspiffy.coordinatoris aWalletCoordinator, not anActorRef.tellworks as before. Change any variable or field typedActorRefthat holds it.queryIdis nowrequestIdonGetBalanceQuery,GetTransactionsQuery,GetTransactionDetailQuery,ExportTransactionQuery,GetDeferredPaymentsQuery,GetHeaderSyncStatusQueryand their replies. On a reply,requestIdis nullable.- Replace your own “send, then wait for the matching event” helpers with
ask. Each request now gets exactly one reply carrying itsrequestId, and anErrorEventit causes carries it too. - Some commands have new replies.
DeleteWalletCommandis answered withWalletDeletedEvent,ReleaseUTXOsCommandwithUTXOsReleasedEvent,AcceptChannelCommandwithChannelAcceptedEvent,RejectChannelCommandwithChannelRejectedEventandExpireChannelCommandwithChannelExpiredEvent. A refusal ofDeleteWalletCommand,ReleaseUTXOsCommandorRecordOutgoingCommandis the reply’s failure, no longer anErrorEvent. ImportWalletCommandwith a mnemonic imports the wallet’s history, answered withImportCompleteEvent. In 4.x it only created the wallet. Without ablockchainDataSourceit fails.TimestampCommandis answered with ARC’s answer to the broadcast, not when the broadcast is handed to ARC.- Removed:
RefreshWalletCommand,NodeRpcDataSource(useWhatsOnChainDataSourceor your ownBlockchainDataSource),LibSpiffyActorSystem.broadcastChannelEvent, thePaymentReadyEvent.errorandProvisioningCompleteEvent.errorconstructors, andCdnHeaderSyncConfig.concurrentDownloads.
The full list, including the removed Isar where clauses and actor constructor parameters, is in the 5.0.0 entry of the changelog.
- Follow the tutorial to fund a wallet and send an SPV payment end to end.
- Read about event type registration before you add events of your own.