Summary
When Mostro's Lightning payment to the buyer fails, the order stays at status settled-hold-invoice and Mostro asks the buyer for a new invoice via the add-invoice action. If the buyer runs restore session before responding, the client rebuilds the order state only from the snapshot status and shows the "released / paying sats" screen. The buyer is stuck with no indication that a new invoice is needed.
Before the restore the state was correct (payment-failed, which offers the manual invoice input). Restore regresses a correct state into a wrong one.
Steps to reproduce
Requires running mostrod with the restore-with-AddInvoice support from MostroP2P/mostro#754.
- Complete a buy until the seller releases (
active → fiat-sent → release).
- Make Mostro's LN payout to the buyer fail. The order goes to
settled-hold-invoice; after retries Mostro sends add-invoice asking for a new invoice.
- As the buyer, don't respond and run Restore session.
- Actual: the "released / paying sats" screen is shown. Expected: the new-invoice prompt (manual invoice input), as shown before the restore.
Root cause
settled-hold-invoice is overloaded: for the buyer it covers both "hold settled, sats in flight" and "payout failed, awaiting a new invoice". The protocol distinguishes them only via the action (payment-failed / add-invoice) — payment-failed is not even a protocol Status, it's a client-synthesized status. So the daemon always reports settled-hold-invoice for these orders, and the daemon re-sends AddInvoice to the buyer's trade key on restore ([mostro#754]MostroP2P/mostro#754)) precisely so the client can act on it.
The restore rebuild derives state purely from the snapshot status:
lib/features/restore/restore_manager.dart:519-524
case Status.settledHoldInvoice:
return userRole == Role.buyer
? Action.released // always "paying sats"
: Action.holdInvoicePaymentSettled;
Applied in restore() (restore_manager.dart:663-826) via notifier.updateStateFromMessage(...). Because the daemon always reports settled-hold-invoice, a status-based rebuild can never recover the failed-payout substate.
The re-sent add-invoice messages do arrive and get saved, but they're dropped by the restore flow and ignored by the rebuild.
Contributing factors that stop the real add-invoice from fixing it during restore:
abstract_mostro_notifier.dart:83-87 — the isRestoring gate skips all message processing.
order_state.dart:289-295 — add-invoice only keeps paymentFailed if the state was already paymentFailed.
Proposed fix
Have the restore rebuild use the snapshot only to seed session/role/peer, and derive the final state by replaying the stored messages (like OrderNotifier.sync()), so the re-sent payment-failed/add-invoice win and the order lands on payment-failed → manual invoice input.
Minimal alternative: in _getActionFromStatus, for settledHoldInvoice + buyer, check stored history; if there's a payment-failed/add-invoice after the released, map to Action.paymentFailed instead of Action.released.
Acceptance criteria
Summary
When Mostro's Lightning payment to the buyer fails, the order stays at status
settled-hold-invoiceand Mostro asks the buyer for a new invoice via theadd-invoiceaction. If the buyer runs restore session before responding, the client rebuilds the order state only from the snapshotstatusand shows the "released / paying sats" screen. The buyer is stuck with no indication that a new invoice is needed.Before the restore the state was correct (
payment-failed, which offers the manual invoice input). Restore regresses a correct state into a wrong one.Steps to reproduce
Requires running mostrod with the restore-with-AddInvoice support from MostroP2P/mostro#754.
active→fiat-sent→ release).settled-hold-invoice; after retries Mostro sendsadd-invoiceasking for a new invoice.Root cause
settled-hold-invoiceis overloaded: for the buyer it covers both "hold settled, sats in flight" and "payout failed, awaiting a new invoice". The protocol distinguishes them only via the action (payment-failed/add-invoice) —payment-failedis not even a protocolStatus, it's a client-synthesized status. So the daemon always reportssettled-hold-invoicefor these orders, and the daemon re-sendsAddInvoiceto the buyer's trade key on restore ([mostro#754]MostroP2P/mostro#754)) precisely so the client can act on it.The restore rebuild derives state purely from the snapshot status:
lib/features/restore/restore_manager.dart:519-524Applied in
restore()(restore_manager.dart:663-826) vianotifier.updateStateFromMessage(...). Because the daemon always reportssettled-hold-invoice, a status-based rebuild can never recover the failed-payout substate.The re-sent
add-invoicemessages do arrive and get saved, but they're dropped by the restore flow and ignored by the rebuild.Contributing factors that stop the real
add-invoicefrom fixing it during restore:abstract_mostro_notifier.dart:83-87— theisRestoringgate skips all message processing.order_state.dart:289-295—add-invoiceonly keepspaymentFailedif the state was alreadypaymentFailed.Proposed fix
Have the restore rebuild use the snapshot only to seed session/role/peer, and derive the final state by replaying the stored messages (like
OrderNotifier.sync()), so the re-sentpayment-failed/add-invoicewin and the order lands onpayment-failed→ manual invoice input.Minimal alternative: in
_getActionFromStatus, forsettledHoldInvoice+ buyer, check stored history; if there's apayment-failed/add-invoiceafter thereleased, map toAction.paymentFailedinstead ofAction.released.Acceptance criteria
settled-hold-invoicewith a failed payout, the buyer sees the new-invoiceprompt, not "paying sats".
settled-hold-invoicewith no failed payout) still shows "released / paying sats".settled-hold-invoice+payment-failed/add-invoicein storage → final statepayment-failed.