Skip to content

fix(#203): stop the public order status from driving trade actions - #271

Merged
Catrya merged 5 commits into
mainfrom
fix/203-trade-status-desync
Aug 5, 2026
Merged

fix(#203): stop the public order status from driving trade actions#271
Catrya merged 5 commits into
mainfrom
fix/203-trade-status-desync

Conversation

@AndreaDiazCorreia

@AndreaDiazCorreia AndreaDiazCorreia commented Aug 5, 2026

Copy link
Copy Markdown
Member

Closes #203.

Root cause

The s tag of a Kind 38383 event is NIP-69's four-bucket public view (pending, in-progress, success, canceled), and mostrod stops publishing once a trade turns private — active, fiat-sent and dispute never reach the wire. Both ingest paths wrote that bucket straight into the trade row and the order book, dragging a trade that daemon messages had already advanced to Active back to InProgress, which the UI presented as an active trade.

That is what offered Open dispute (and Mark fiat sent) on a trade the daemon rejects with CantDo: it only accepts either on an Active or FiatSent order. So the gating rule was right; the status feeding it was not — status desync, not a gating mismatch (answers the issue's needs verification).

What changed

  • RustBuyerTookOrder / HoldInvoicePaymentAccepted now imply Active in status_for_action; a wire status only applies when no finer local one is known or when it is terminal (wire_status_applies); open_dispute checks the status it already holds before publishing and fails with the TradeNotDisputable marker.
  • Dartin-progress gets its own TradeStatus: waiting presentation, no dispute and no fiat-sent, cancel still available since
    the daemon accepts it there.
  • Docs — the public-vs-trade status rule in the orders contract, OrderStatus, the state machine, the open_dispute precondition and CLAUDE.md.

Verification

cargo test and cargo clippy (24 warnings, same as main), flutter analyze clean, flutter test 197 passed. Three Rust tests and two widget tests added.

Notes

Summary by CodeRabbit

  • New Features

    • Added a clear “Trade setup in progress” state with waiting messaging and appropriate actions.
    • Trade status now remains accurate instead of being shown as active prematurely.
    • Dispute actions are unavailable until the trade reaches an eligible state.
  • Bug Fixes

    • Improved handling of trade status updates to prevent incorrect state changes.
    • Added a specific message when disputes are attempted too early.
    • Improved status updates for buyer and seller actions.
  • Localization

    • Added corresponding trade-status and dispute messages in English, German, Spanish, French, and Italian.
  • Documentation & Tests

    • Updated status behavior documentation and expanded coverage for in-progress trades.

…s to Active status

Extend status_for_action to recognize BuyerTookOrder and HoldInvoicePaymentAccepted as Active status indicators. Use or_else fallback when syncing order status from peer pubkey messages to handle payloads without explicit status field. Add test coverage for escrow-locked action mappings.
…rained trade status

Add wire_status_applies guard to block public NIP-69 status updates (pending/in-progress/success/canceled) from replacing daemon-sourced states like Active or FiatSent. Public events stop updating once trades turn private, so in-progress only means "taken" and must not drag Active trades back.
Add TradeNotDisputable error handling to prevent opening disputes before escrow is locked. Check local trade status against Active/FiatSent/InProgress states before dispatching to daemon. Add localized error messages across all supported languages (en/de/es/fr/it) and test coverage for status validation.
…ase actions

Introduce TradeStatus.inProgress enum value to represent NIP-69's coarse "in-progress" bucket when the order left the public book but escrow state is unknown. Map OrderStatus.inProgress separately from Active instead of conflating them. Display waiting state UI with localized "Setting up the trade" messages across all languages.
…e status

Document that NIP-69 `s` tag values (pending/in-progress/success/canceled) are coarse public view only. Daemon stops publishing after trades turn private, so InProgress means "taken, state unknown" not "escrow locked". Add wire_status_applies guard requirement to prevent public events from overwriting daemon-sourced states.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d9c41a66-2e93-4bd1-8573-9ada621fc52d

📥 Commits

Reviewing files that changed from the base of the PR and between a149b8f and 6772aa6.

📒 Files selected for processing (21)
  • CLAUDE.md
  • lib/core/daemon_errors.dart
  • lib/features/trades/screens/trade_detail_screen.dart
  • lib/l10n/app_de.arb
  • lib/l10n/app_en.arb
  • lib/l10n/app_es.arb
  • lib/l10n/app_fr.arb
  • lib/l10n/app_it.arb
  • lib/l10n/app_localizations.dart
  • lib/l10n/app_localizations_de.dart
  • lib/l10n/app_localizations_en.dart
  • lib/l10n/app_localizations_es.dart
  • lib/l10n/app_localizations_fr.dart
  • lib/l10n/app_localizations_it.dart
  • rust/src/api/disputes.rs
  • rust/src/api/orders.rs
  • specs/004-mostro-p2p-client/contracts/disputes.md
  • specs/004-mostro-p2p-client/contracts/orders.md
  • specs/004-mostro-p2p-client/contracts/types.md
  • specs/004-mostro-p2p-client/data-model.md
  • test/features/trades/trade_detail_screen_test.dart

Walkthrough

The client now distinguishes public InProgress order status from Active, preserves daemon-derived states during wire ingestion, validates dispute eligibility, and updates trade-detail UI and localization for setup-in-progress trades.

Changes

Trade state reconciliation

Layer / File(s) Summary
Public and local status reconciliation
rust/src/api/orders.rs, specs/004-mostro-p2p-client/..., CLAUDE.md
Kind 38383 updates no longer overwrite finer daemon-derived statuses. Escrow-lock actions map to Active, and terminal public statuses remain applicable.
Dispute precondition validation
rust/src/api/disputes.rs, lib/core/daemon_errors.dart, specs/004-mostro-p2p-client/contracts/disputes.md
Dispute initiation checks local status and returns TradeNotDisputable when the daemon precondition is not met.
InProgress trade-detail behavior
lib/features/trades/screens/trade_detail_screen.dart, lib/l10n/*, test/features/trades/trade_detail_screen_test.dart
InProgress has separate labels, waiting-state presentation, cancellation behavior, disabled setup CTA, and buyer and seller widget coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PublicOrderEvent
  participant OrderIngestion
  participant LocalTradeStatus
  participant TradeDetailScreen
  PublicOrderEvent->>OrderIngestion: provide coarse Kind 38383 status
  OrderIngestion->>LocalTradeStatus: compare persisted daemon-derived status
  LocalTradeStatus-->>OrderIngestion: retain finer status when applicable
  OrderIngestion->>TradeDetailScreen: expose reconciled order status
  TradeDetailScreen-->>TradeDetailScreen: render InProgress setup state
Loading

Possibly related issues

  • Issue 203: The dispute validation and separate InProgress UI state address premature dispute actions rejected by the daemon.
  • MostroP2P/app issue 269: Both changes preserve persisted local order states during Kind 38383 ingestion.

Possibly related PRs

Poem

A rabbit sees the trade begin,
“In progress” waits before the win.
Wire states hop, but do not erase
The daemon’s finer, private trace.
Disputes wait till funds are tight—
Setup sleeps beneath moonlight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary fix: preventing public order status from controlling trade actions.
Linked Issues check ✅ Passed The changes satisfy [#203] by preserving daemon-derived states and preventing Open dispute before the trade is disputable.
Out of Scope Changes check ✅ Passed The status handling, dispute validation, UI changes, documentation, localization, and tests directly support [#203].
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/203-trade-status-desync

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — reviewed at head 6772aa63.

Verification

  • CI green on this head: Rust (build/test/clippy/wasm), Flutter (analyze/test), Web (wasm build).
  • Local: full cargo test --lib — 238 passed, 0 failed, including the three new tests (escrow_locked_actions_imply_active, the_public_status_never_replaces_a_finer_local_one, only_a_funded_trade_is_disputable).
  • git diff --check clean.

Review

  • The root-cause determination answers #203's needs verification correctly: the s tag is NIP-69's coarse bucket and mostrod never publishes active/fiat-sent/dispute, so this was status desync, not a gating mismatch.
  • wire_status_applies is the right invariant: wire may fill None/Pending or announce a terminal status, but never overwrite a finer daemon-learned state. Both ingest paths (ingest_order_event, subscribe_single_order) apply it consistently, and in both the book is repaired to the local status when the wire loses (order.status = local before upsert).
  • BuyerTookOrder / HoldInvoicePaymentAccepted implying Active matches the daemon semantics (escrow locked is exactly what those two messages mean for each side).
  • The taker-timeout republish case stays safe: ingest_order_event skips the gate entirely for a pending wire status, so a republished order returns to the book normally.
  • Defense-in-depth in open_dispute (fail before publish with the TradeNotDisputable marker, surfaced via localizedDaemonError) plus the new TradeStatus.inProgress UI state (waiting presentation, cancel kept, no dispute/fiat-sent) match the daemon's preconditions. Widget tests cover both buyer and seller.
  • Docs (contracts, data-model, CLAUDE.md) and l10n (en/de/es/fr/it) are in sync.

Non-blocking notes

  • In subscribe_single_order, a republished pending wire event while the local trade is in a waiting state is masked in the book (order.status = local). Harmless today — the public list filters non-pending and the Canceled gift wrap reconciles the trade — just noting the small window.
  • status_allows_dispute deliberately lets InProgress through to the daemon. Reasonable as a backstop since the real state is unknown; the UI already hides the action there.
  • The stated overlap with #213 in trade_detail_screen.dart is real — whichever merges second will need a small rebase in the status maps.

@Catrya Catrya left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK

@Catrya
Catrya merged commit 4cc4219 into main Aug 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trade Detail offers Open dispute before the trade is disputable (daemon rejects with CantDo)

2 participants