Skip to content

Queries

This page lists the queries you can send to the wallet coordinator in libspiffy 5.0.0, the response that answers each one, and how to wait for it with ask(). Queries read state; they change nothing. They are defined in lib/src/actors/coordinator_messages.dart and exported by package:libspiffy/coordinator.dart.

Query Response Failure Default timeout
GetBalanceQuery BalanceResponse ErrorEvent, source getBalance 1 minute
GetTransactionsQuery TransactionsResponse ErrorEvent, source getTransactions 1 minute
GetTransactionDetailQuery TransactionDetailResponse ErrorEvent, source getTransactionDetail 1 minute
ExportTransactionQuery TransactionExportedEvent TransactionExportedEvent with success: false 1 minute
GetDeferredPaymentsQuery DeferredPaymentsResponse ErrorEvent, source getDeferredPayments 1 minute
GetHeaderSyncStatusQuery HeaderSyncStatusResponse none 1 minute

Every query is a CoordinatorRequest<R>, where R is its response type. libspiffy.coordinator.ask() sends the query and completes with its own response, whatever else is on the event stream:

// Sketch: assumes an initialized LibSpiffyActorSystem named `libspiffy`.
import 'dart:async';
import 'package:libspiffy/coordinator.dart';
Future<void> showBalance(String walletId) async {
try {
final balance = await libspiffy.coordinator.ask(GetBalanceQuery(walletId: walletId));
print('spendable: ${balance.totalBalance} sats');
} on CoordinatorFailure catch (failure) {
print('no balance: ${failure.message}');
} on TimeoutException {
print('no answer within a minute');
}
}
  • requestId. Every query takes an optional requestId. When you leave it out, a unique one is generated when the query is made. The response carries it back, so two queries of the same kind running at once each get their own answer. In 4.x this field was queryId; 5.0.0 renames it to requestId on every query and response.
  • Failures. A query that cannot be answered produces an ErrorEvent that carries the query’s requestId. ask() then throws CoordinatorFailure, whose event is the ErrorEvent. ExportTransactionQuery reports its failure in its own response instead, and ask() throws CoordinatorFailure with that response as event.
  • Timeouts. ask() throws TimeoutException when no response arrives within the query’s replyTimeout (one minute for every query). Pass timeout: to change it.
  • The event stream. Responses are still published on libspiffy.coordinatorEvents, so a listener there sees them too.

Reads a wallet’s balances from the read model.

Field Type Required
walletId String yes
requestId String? no

Response: BalanceResponse. confirmedBalance and unconfirmedBalance are the spendable payment UTXOs split by whether they have a block height; totalBalance is their sum. pendingBalance (outputs the network is not known to hold, or whose proof a reorg removed), watchOnlyBalance (watch addresses) and reservedBalance (reserved UTXOs, including a deferred payment’s held inputs) are the wallet’s too, but are not part of totalBalance.

Field Type
walletId String
requestId String?
confirmedBalance BigInt
unconfirmedBalance BigInt
totalBalance BigInt
pendingBalance BigInt
watchOnlyBalance BigInt
reservedBalance BigInt

On failure: ErrorEvent with source getBalance, the walletId and the query’s requestId.

To hear about balance changes without polling, follow BalanceUpdatedEvent, which carries the same numbers:

libspiffy.coordinator
.on<BalanceUpdatedEvent>(walletId: 'alice')
.listen((e) => print('alice: ${e.totalBalance} sats'));

Reads a page of a wallet’s transaction history.

Field Type Required
walletId String yes
limit int no (default 50)
offset int no (default 0)
requestId String? no

Response: TransactionsResponse. transactions is a list of BitcoinTransaction (exported by package:libspiffy/libspiffy.dart).

Field Type
walletId String
requestId String?
transactions List<BitcoinTransaction>

On failure: ErrorEvent with source getTransactions, the walletId and the query’s requestId.

Reads one transaction of a wallet.

Field Type Required
walletId String yes
txid String yes
requestId String? no

Response: TransactionDetailResponse. When the wallet has no such transaction, found is false and transaction is null. That is an answer, not a failure: ask() returns it.

Field Type
walletId String
requestId String?
transaction BitcoinTransaction?
found bool
error String?

On failure: ErrorEvent with source getTransactionDetail, the walletId and the query’s requestId.

Exports a transaction of the wallet as BEEF with its merkle proof, for another wallet to import with ImportTransactionCommand. This is the sending side of a hand-off: a service’s xpub wallet passing on a payment it received for an offline payee, or a payer passing on a type-42 payment it broadcast itself.

Field Type Required
walletId String yes
txid String yes
requestId String? no

Response: TransactionExportedEvent. beef is the BEEF bytes. delegatedIndices and type42Derivations are what the importing wallet passes to ImportTransactionCommand.

Field Type
walletId String
txid String
requestId String?
success bool
beef List<int>?
delegatedIndices List<int>
type42Derivations List<Type42Derivation>
error String?

On failure: TransactionExportedEvent with success: false and error. The export is refused while the transaction has no proof verified against the local header chain.

// Sketch: hand a proven payment from one wallet to another.
final export = await libspiffy.coordinator
.ask(ExportTransactionQuery(walletId: 'service', txid: txid));
await libspiffy.coordinator.ask(ImportTransactionCommand(
walletId: 'payee',
beef: export.beef!,
delegatedIndices: export.delegatedIndices,
type42Derivations: export.type42Derivations,
));

Lists or searches a wallet’s deferred payments: payments built by PayInvoiceCommand (and Benford split transactions, which are recorded the same way) that the network is not known to hold. By default it returns outstanding payments only, newest first, 50 per page, each with its BEEF rebuilt from storage. Use olderThan or createdBefore to find payments the recipient has not broadcast, and includeResolved or states to include seen, mined, failed, cancelled, reclaimed and completed ones. Nothing is ever deleted.

Field Type Required
walletId String yes
states Set<DeferredPaymentState>? no
includeResolved bool no (default false)
createdBefore DateTime? no
createdAfter DateTime? no
olderThan Duration? no
lastNetworkStatuses Set<String>? no
invoiceId String? no
recipientAddress String? no
dueBefore DateTime? no
limit int no (default 50)
cursor String? no
oldestFirst bool no (default false)
includeBeef bool no (default true)
requestId String? no

Response: DeferredPaymentsResponse. nextCursor is the cursor for the next page, or null on the last page.

Field Type
walletId String
requestId String?
payments List<DeferredPaymentDetail>
nextCursor String?

On failure: ErrorEvent with source getDeferredPayments, the walletId and the query’s requestId.

states overrides includeResolved. olderThan and createdBefore combine; the earlier bound wins. limit is 1 to 1000. Set includeBeef: false to skip rebuilding each BEEF when you only need the list.

Each DeferredPaymentDetail has payment (DeferredPayment), rawTxHex (String?), beef (Uint8List?) and beefError (String?, why beef is null although it was requested). It also forwards the payment’s fields: txid, invoiceId, recipientAddresses, amount, fee, createdAt, heldInputs, lastNetworkStatus, lastCheckedAt, competingTxids, state, purpose and resolutionReason, plus reclaimsTxid and isReclaim for a reclaim’s self-spend.

Page through every outstanding payment:

// Sketch: collect all outstanding deferred payments of a wallet.
Future<List<DeferredPaymentDetail>> outstanding(String walletId) async {
final all = <DeferredPaymentDetail>[];
String? cursor;
do {
final page = await libspiffy.coordinator.ask(GetDeferredPaymentsQuery(
walletId: walletId,
cursor: cursor,
includeBeef: false,
));
all.addAll(page.payments);
cursor = page.nextCursor;
} while (cursor != null);
return all;
}

Act on a payment with the deferred-payment commands.

Asks where header sync stands. The coordinator passes it to the header sync actor.

Field Type Required
requestId String? no

Response: HeaderSyncStatusResponse. status is a HeaderSyncStatus with height (the active chain’s tip), networkHeight (the higher of height and the heights peers reported when they connected; 0 while none did), synced (the last peer answer held fewer than 2,000 headers) and peerCount.

Field Type
requestId String?
status HeaderSyncStatus

On failure: no failure event.

final status = (await libspiffy.coordinator.ask(GetHeaderSyncStatusQuery())).status;

To follow sync without polling, listen for HeaderSyncStatusEvent (emitted when synced changes) and BlockHeadersStoredEvent (each batch stored).