Deferred payments
This page shows how to manage a payment after PayInvoiceCommand has built it and you have handed the BEEF
to the recipient: check what the network knows, broadcast it yourself, cancel it, reclaim the coins, or
complete a transaction the counterparty co-signs.
Why a payment is “deferred”
Section titled “Why a payment is “deferred””PayInvoiceCommand signs a payment but does not broadcast it. The recipient normally does (see
Invoices & SPV). Until the network has the transaction, the payer’s wallet
holds its inputs: they are reserved for that transaction with no expiry, and neither the reservation
cleanup nor another payment can take them. Its change is not spendable yet either.
The hold ends when:
- ARC reports the transaction
SEEN_ON_NETWORK,SEEN_MULTIPLE_NODESorMINED: the inputs are spent and the change becomes available; - ARC reports
REJECTED, or the wallet proves an input was already spent in a block (INPUT_SPENT): the payment fails and the inputs are released; - you cancel it, or your reclaim of it reaches the network.
DOUBLE_SPEND_ATTEMPTED is not a verdict: either transaction may still be mined, so the payment stays
outstanding with its inputs held. Every step is journaled, and nothing is deleted: resolved payments stay
listable.
States
Section titled “States”DeferredPaymentState:
| State | Meaning |
|---|---|
outstanding |
Signed and handed over; not known to be on the network. Inputs held. |
seen |
On the network. Not yet a confirmation. |
mined |
Confirmed by a merkle proof checked against your header chain. |
failed |
REJECTED (or an input proven spent); inputs released. |
cancelled |
You cancelled it while the network did not know it; inputs released. |
reclaimed |
Your self-spend of its inputs is on the network. |
completed |
A half-signed payment the counterparty completed; the completed transaction took over the hold. |
Asking and answers
Section titled “Asking and answers”Each command on this page is a CoordinatorRequest: send it with coordinator.ask, which completes with its
own reply (see Waiting for an answer). The reply carries
the command’s requestId, which you can pass or leave to be generated.
| Request | Reply | Default timeout |
|---|---|---|
GetDeferredPaymentsQuery |
DeferredPaymentsResponse |
1 minute |
CheckDeferredPaymentStatusCommand |
DeferredPaymentStatusEvent |
3 minutes |
BroadcastDeferredPaymentCommand |
DeferredPaymentBroadcastEvent |
3 minutes |
CancelDeferredPaymentCommand |
DeferredPaymentCancelledEvent |
3 minutes |
ReclaimDeferredPaymentCommand |
DeferredPaymentReclaimedEvent |
6 minutes |
CompleteDeferredPaymentCommand |
DeferredPaymentCompletedEvent |
1 minute |
A reply with success: false makes ask throw CoordinatorFailure. To read the fields of a failed reply
(the network status, the competing txids), take it from the failure’s event:
try { final cancelled = await alice.coordinator.ask(CancelDeferredPaymentCommand( walletId: 'alice-wallet', txid: txid, reason: 'customer left', )); print('released ${cancelled.releasedUtxoKeys}');} on CoordinatorFailure catch (failure) { final refused = failure.event; if (refused is DeferredPaymentCancelledEvent) { print('not cancelled (${refused.networkStatus}): ${refused.error}'); }}Each answer is emitted once the wallet’s read model shows its effect, so a query you make on hearing it sees the new status, the spent inputs and the change.
List payments
Section titled “List payments”final page = await alice.coordinator.ask(GetDeferredPaymentsQuery( walletId: 'alice-wallet', olderThan: const Duration(hours: 1), // not broadcast after an hour));for (final p in page.payments) { print('${p.txid} ${p.state} ${p.lastNetworkStatus}');}// page.nextCursor: pass it as `cursor` for the next page; null on the last one.By default the query lists outstanding payments only, newest first, 50 per page (limit, 1 to 1000), each
with its BEEF rebuilt from storage (includeBeef: true). Filters: states or includeResolved,
createdBefore, createdAfter, olderThan, lastNetworkStatuses, invoiceId, recipientAddress,
dueBefore (deadline at or before), cursor and oldestFirst.
Each DeferredPaymentDetail has txid, invoiceId, recipientAddresses, amount, fee, createdAt,
heldInputs, lastNetworkStatus, lastCheckedAt, competingTxids, state, purpose,
resolutionReason, rawTxHex, beef and beefError. A reclaim’s own self-spend is listed too:
isReclaim is true and reclaimsTxid names the payment it reclaims.
Check the status now
Section titled “Check the status now”The wallet’s ARC actor asks ARC about outstanding payments on a schedule. To ask now:
final status = await alice.coordinator.ask( CheckDeferredPaymentStatusCommand(walletId: 'alice-wallet', txid: txid));print('${status.networkStatus} via ${status.source}, confirmed: ${status.confirmed}');via chooses the source: DeferredPaymentNetworkSource.arc (the default), dataSource (the configured
BlockchainDataSource), or arcThenDataSource. The wallet is updated as for a scheduled check. A MINED
answer confirms the payment only when its merkle proof matches your local headers, whichever source gave it.
DeferredPaymentStatusEvent carries success (a source answered), networkStatus, source, blockHeight,
proofStatus (verified, headerUnknown, rootMismatch, malformed, or null), confirmed,
competingTxids and error. NOT_FOUND is not a failure: the recipient may still broadcast. ask throws
only when no source answered.
Broadcast it yourself
Section titled “Broadcast it yourself”When the recipient is slow:
final sent = await alice.coordinator.ask( BroadcastDeferredPaymentCommand(walletId: 'alice-wallet', txid: txid));print('${sent.networkStatus} via ${sent.source}');The payment’s unproven ancestors, from the BEEF rebuilt from storage, go first. The command is idempotent: a
transaction the network already has is reported as such. With via including the data source, a
transaction ARC refuses is submitted to the BlockchainDataSource.
success means the network holds the transaction. If ARC answers while still processing (Arcade answers
every submission RECEIVED), the wallet asks again after 1, 2, 4, 8 and 15 seconds, about 30 seconds in all,
and stops at the first answer that is a verdict. A transaction still in flight then, in the orphan mempool,
contested or rejected is reported with success: false and the reason in error, so ask throws.
willRetry on that failed reply says the broadcast was queued for a durable retry.
Cancel it
Section titled “Cancel it”The sample under Asking and answers cancels a payment. On success,
DeferredPaymentCancelledEvent.releasedUtxoKeys lists the inputs released.
The network is checked first. The cancellation is refused when the network knows the transaction (any status
except NOT_FOUND and DOUBLE_SPEND_ATTEMPTED), and when the check fails, unless you pass force: true.
force never cancels a transaction the network knows.
Reclaim it
Section titled “Reclaim it”try { final reclaim = await alice.coordinator.ask(ReclaimDeferredPaymentCommand( walletId: 'alice-wallet', txid: txid, reason: 'recipient never broadcast it', // optional, recorded as the resolution reason )); print('${reclaim.reclaimedSatoshis} sats back in ${reclaim.reclaimTxid}, fee ${reclaim.fee}');} on CoordinatorFailure catch (failure) { final lost = failure.event; if (lost is DeferredPaymentReclaimedEvent) { print('reclaim ${lost.reclaimTxid} not taken; competing: ${lost.competingTxids}'); }}A reclaim spends the held inputs back to a wallet address and broadcasts that self-spend at once. There is
no confirmation step and no dry run: warn your user first if your app wants that. The self-spend pays ARC’s
standard policy fee. BSV has no replace-by-fee, and the first spend seen wins: if the recipient’s copy
reached the network first, the reclaim is the one refused, and competingTxids names their transaction.
DeferredPaymentReclaimedEvent carries txid, requestId, reclaimTxid, success, reclaimedUtxoKeys,
reclaimedSatoshis (the held satoshis less the fee), fee, toAddress, networkStatus, source,
competingTxids and error.
The payment becomes reclaimed only when the network has the self-spend, never at broadcast. If the
reclaim’s answer is not a success (ask throws), the payment stays outstanding with its inputs now held by the self-spend.
It resolves as reclaimed if the network reports the self-spend later; watch reclaimTxid with
CheckDeferredPaymentStatusCommand. If the self-spend fails (a competing spend wins, or ARC answers
REJECTED), its inputs go back to the payment it reclaimed, which is plain outstanding again.
Reclaim at a deadline
Section titled “Reclaim at a deadline”Give PayInvoiceCommand a deadline (a UTC DateTime) and the wallet reclaims the payment by itself if it is
still outstanding then. The coordinator sweeps every wallet once a minute
(LibSpiffyActorSystem.initialize(deadlineSweepInterval:)) and answers each reclaim with a
DeferredPaymentReclaimedEvent whose requestId is deadline-<txid>. No request of yours asked for it, so
follow these with on:
alice.coordinator .on<DeferredPaymentReclaimedEvent>(walletId: 'alice-wallet') .where((e) => e.requestId?.startsWith('deadline-') ?? false) .listen((e) => print('deadline reclaim of ${e.txid}: ${e.success ? 'taken' : e.error}'));``` A payment the network took, that itscounterparty completed, or that you cancelled before the deadline is left alone. Until the reclaim is mined,the holder can still broadcast the original, and the two race as any reclaim does.
## Complete a half-signed payment
A sale in one transaction is signed by two wallets. The first signs its own inputs and records a half thatcannot be broadcast yet; it holds those inputs as a deferred payment. When the counterparty returns thetransaction with their signatures added:
```dartfinal completed = await alice.coordinator.ask(CompleteDeferredPaymentCommand( walletId: 'alice-wallet', txid: halfTxid, rawHex: completedTxHex,));print('the hold moved to ${completed.completedTxid}');The wallet refuses unless the completed transaction has the same version, lock time, inputs, sequences and
outputs, and your own unlocking scripts unchanged. The completed transaction takes over the hold and the half
becomes completed. Nothing is broadcast: settle the completed transaction as any other deferred payment,
or let the counterparty broadcast it.
Double spends on Arcade
Section titled “Double spends on Arcade”Arcade, the Teranode-era ARC, refuses a later double spend outright with REJECTED, where ARC held it as
DOUBLE_SPEND_ATTEMPTED. It likewise refuses a spend of an output already spent in a block. The localnet
tests (test/integration/localnet_deferred_e2e_test.dart) show what each wallet sees:
- You reclaim first, the recipient submits later. Your reclaim succeeds and the payment is
reclaimed. The recipient’sValidateBEEFCommandstill answersvalid: true(the BEEF is well formed), but withnetworkStatus: REJECTED; their balance does not count it and their invoice is not paid. - The recipient submits first, you reclaim later. Your reclaim answers
success: false, soaskthrows. Either the wallet already heard the payment is on the network and builds no reclaim (“not outstanding”), or the reclaim is refused asREJECTEDorDOUBLE_SPEND_ATTEMPTEDwith the payment incompetingTxids. When the block arrives, both wallets record the recipient’s copy. - You cancel, the recipient broadcasts later. Your funds are released at once. Once the recipient’s copy
is on the network, a status check of the payment finds it there: the payment becomes
seen, your inputs are spent and only the change is yours.
A REJECTED answer is final: the payment is failed and its inputs released. DOUBLE_SPEND_ATTEMPTED
keeps the payment outstanding, so you may see either depending on which ARC you use.
Related
Section titled “Related”- Invoices & SPV
- Payment channels: a channel’s funding inputs are held the same way
- API reference