Skip to content

Restore shows "released / paying sats" instead of the new-invoice prompt when the LN payout failed #615

Description

@Catrya

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.

  1. Complete a buy until the seller releases (activefiat-sent → release).
  2. 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.
  3. As the buyer, don't respond and run Restore session.
  4. 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-295add-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

  • After restoring an order in settled-hold-invoice with a failed payout, the buyer sees the new-invoice
    prompt, not "paying sats".
  • The happy path (settled-hold-invoice with no failed payout) still shows "released / paying sats".
  • Post-restore state matches pre-restore state for this order.
  • Regression test: restore with snapshot settled-hold-invoice + payment-failed/add-invoice in storage → final state payment-failed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions