Skip to content

fix: take_order and send_invoice wait for daemon confirmation#178

Merged
Catrya merged 7 commits into
mainfrom
fix-take-order
Jul 11, 2026
Merged

fix: take_order and send_invoice wait for daemon confirmation#178
Catrya merged 7 commits into
mainfrom
fix-take-order

Conversation

@Catrya

@Catrya Catrya commented Jul 11, 2026

Copy link
Copy Markdown
Member

Closes #170

Problem

Taking an order and submitting an invoice were fire-and-forget: take_order
returned Ok(trade) unconditionally (even on internal errors and publish
failures) and persisted a local trade before any daemon response. When the
daemon rejected the take (CantDo) or never answered, the app still walked
the whole happy path — add-invoice screen, AddInvoice publish the daemon
errored on, "Waiting for a counterpart" — for a trade that never existed.
Daemon rejections found no waiter and were dropped as "stale".

Fix

Extends the request-correlation machinery from #172 (create_order) to the
take and add-invoice flows, matching how mostro-cli and MostriX correlate
every action:

  • refactor(orders): generalize PendingCreatePendingRequest with a
    kind per request type (Create / Take / AddInvoice). No behavior
    change; same correlation invariant.
  • fix(orders) take_order: sends a request_id nonce and waits up to 10 s
    for the daemon's first reply. A take's success reply varies by role and
    daemon config, so replies are classified by payload shape (the MostriX
    pattern): add-invoice → calculated sats, pay-invoice → hold invoice,
    pay-bond-invoice → stable BondRequired rejection (bonds unsupported).
    The trade is built from the reply's real data and nothing is persisted
    on rejection, timeout, internal error or publish failure
    . All error
    paths now return Err (localized in the take screen).
  • fix(orders) send_invoice: same nonce + 10 s wait. A correlated CantDo
    (e.g. InvalidInvoice) or timeout surfaces as an error and the UI stays
    on the invoice step. The reply doubles as a status update, so the
    dispatcher acknowledges the waiter and falls through to the normal arms.
  • feat(ui): the add-invoice and pay-invoice screens now display the
    daemon-calculated sats amount (data this fix captures from the replies);
    take_order also syncs it into the order book so the amount resolves on
    the provider's first poll.
  • docs(specs): orders contract updated — correlation invariant, real
    take_order/send_invoice signatures and error surfaces.
  • fix(ui): the pay-invoice screen now navigates to the trade once the
    daemon registers the payment (waiting-buyer-invoice), not only when the
    order goes straight to active.

The correlation invariant is unchanged from #172: only a reply echoing
the exact nonce may resolve or consume a pending request
; stale relay
replays touch nothing.

Verification

cargo test (105) + cargo clippy clean, flutter analyze clean
(3 pre-existing infos). Manual end-to-end run against the test daemon
covered every scenario:

  • Full trade as buyer (take-sell): AddInvoice reply correlated →
    trade built with the daemon's calculated sats → invoice acknowledged via
    WaitingSellerToPay → trade completed through Released /
    PurchaseCompleted / RateReceived.
  • Full trade as seller (take-buy): PayInvoice reply with the hold
    invoice (amount extracted from the embedded order — the fallback path) →
    trade completed through HoldInvoicePaymentSettled / RateReceived.
  • Take timeout: daemon stayed silent for one take → clean
    NoDaemonResponse, no phantom trade persisted, immediate retry
    succeeded. This exact case produced the fake trade before this PR.
  • Invalid invoice: CantDo(InvalidInvoice) echoing the nonce →
    send_invoice returned the error and the UI stayed on the invoice step.
  • Startup history replay: every stale NewOrder/CantDo ignored
    ("no matching pending request").

Summary by CodeRabbit

  • New Features
    • Invoice screens now show the amount to receive or pay in sats.
    • Added localized messaging across supported languages for invoice amounts and bond requirements.
    • Order and invoice actions now wait for confirmed responses before proceeding.
  • Bug Fixes
    • Improved error messages by removing technical prefixes and providing clearer session-timeout and bond-required guidance.
    • Prevented stale or mismatched responses from affecting order and payment actions.
    • Orders already taken now return users to the home screen with a localized message.

Catrya added 5 commits July 10, 2026 19:37
…uests

- rename PendingCreate/DaemonConfirmation to PendingRequest/DaemonReply and
  move the create-specific fields (local_uuid, content_key) into a
  PendingRequestKind::Create variant
- no behavior change: same correlation invariant, same call sites, tests
  adapted mechanically
- prepares the take/add-invoice confirmation work, which reuses this same
  machinery with new request kinds
- send a request_id nonce in take-buy/take-sell and register a pending Take
  record; only the daemon reply echoing the nonce can resolve the caller
- resolve take waiters before the per-action arms: a take's first reply
  varies (add-invoice, pay-invoice, pay-bond-invoice), so replies classify
  by payload shape and the caller applies their effects itself
- return Err on CantDo, internal failures, publish failure and the 10s
  timeout — nothing is persisted until the daemon acknowledges the take,
  removing the fake-trade path
- pay-bond-invoice maps to a stable BondRequired rejection (bonds are not
  supported yet); localized in the take screen along with NoDaemonResponse
  and OrderAlreadyTaken
- send a request_id nonce in add-invoice and register a pending AddInvoice
  record; only the daemon reply echoing the nonce unblocks the caller
- the reply doubles as a status update, so the dispatcher acknowledges the
  waiter and falls through to the per-action arms (unlike takes, whose
  caller applies the reply itself)
- return Err on a correlated CantDo (e.g. invalid invoice) or the 10s
  timeout — the UI stays on the invoice step instead of advancing on a
  publish the daemon errored on
- add-invoice: display the sats the buyer's invoice must be for (from the
  daemon's AddInvoice reply) in the manual entry card
- pay-invoice: display the hold invoice amount (from the PayInvoice reply)
  above the QR in the manual view
- take_order now syncs the reply's amount_sats into the order book too, so
  tradeAmountProvider resolves on its first poll instead of waiting for the
  38383 amt tag
- new addInvoiceAmount/payInvoiceAmount l10n keys in the 5 locales
- document the request_id correlation invariant shared by create/take/
  add-invoice in the orders contract
- take_order: real signature, reply-driven trade construction, and the
  BondRequired/NoDaemonResponse error surface; drop the unimplemented
  ActiveTradeExists precondition
- rename submit_buyer_invoice to the real send_invoice API and describe
  the gated acknowledgement
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Catrya, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 31 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bca15ab-becd-44c9-9391-864b4d87cb90

📥 Commits

Reviewing files that changed from the base of the PR and between 31d87e9 and 7a08f50.

📒 Files selected for processing (2)
  • lib/features/order/screens/pay_lightning_invoice_screen.dart
  • rust/src/api/orders.rs

Walkthrough

Order-taking and invoice submission now wait for nonce-correlated daemon replies before completing. Pending requests share unified matching and timeout handling, while Flutter screens use localized errors and display invoice amounts in sats.

Changes

Order confirmation and correlation

Layer / File(s) Summary
Correlation contracts and message builders
specs/004-mostro-p2p-client/contracts/orders.md, rust/src/mostro/actions.rs
Order and invoice contracts now require correlated request IDs, and action builders propagate them into daemon messages.
Unified pending-request model
rust/src/api/orders.rs
Create, take, and add-invoice requests use shared pending records with nonce-gated matching, waiter detachment, cleanup, and tests for stale or concurrent replies.
Daemon-confirmed order flows
rust/src/api/orders.rs
Create, take, and invoice flows resolve daemon confirmations or rejections, persist accepted trades, synchronize order state, and start sessions only after acceptance.
Localized order and invoice UI
lib/features/order/screens/*, lib/l10n/*
Screens parse Rust errors into localized messages and show localized receive/pay invoice amounts across supported locales.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FlutterScreens
  participant RustOrderAPI
  participant MostroDaemon
  participant TradeSession
  User->>FlutterScreens: Take order or submit invoice
  FlutterScreens->>RustOrderAPI: Invoke correlated order action
  RustOrderAPI->>MostroDaemon: Publish request_id-bearing message
  MostroDaemon-->>RustOrderAPI: Confirm or reject echoed request_id
  RustOrderAPI->>TradeSession: Persist accepted trade and create session
  RustOrderAPI-->>FlutterScreens: Return success or localized error context
Loading

Possibly related PRs

Poem

A bunny hops where invoices glow,
With nonce-bound replies in a tidy row.
No phantom trades can sneak ahead,
Localized sats now shine instead.
“Confirmed!” we cheer, and wiggle our nose!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements correlated take/add-invoice confirmations, errors on rejection/timeout, and only persists after daemon approval, matching #170.
Out of Scope Changes check ✅ Passed The added localization, docs, and request-correlation plumbing are directly tied to the confirmed take/add-invoice flow.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: take_order and send_invoice now wait for daemon confirmation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-take-order

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.

…ayment

- navigate to trade detail on waitingBuyerInvoice: on this screen it can
  only mean the hold invoice was paid and mostrod now waits for the buyer's
  invoice — the seller was left staring at an already-paid QR
- inProgress deliberately stays put: the public 38383 status flips at take
  time, before payment

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/features/order/screens/add_lightning_invoice_screen.dart (1)

93-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared AnyhowException parsing to avoid duplication.

The regex r'^.*?AnyhowException\((.+)\)$' and the extraction logic are duplicated verbatim in take_order_screen.dart (lines 156-157). If the Rust error format changes, both call sites must be updated in lockstep — a missed update would cause inconsistent error display across screens.

Consider extracting to a shared utility, e.g.:

♻️ Proposed refactor
// In a shared utility, e.g. lib/core/error_utils.dart

/// Strips the Rust `AnyhowException(...)` wrapper and returns the inner
/// message, or the raw string if the pattern doesn't match.
String extractAnyhowMessage(String raw) {
  final match = RegExp(r'^.*?AnyhowException\((.+)\)$').firstMatch(raw);
  return match != null ? match.group(1)! : raw;
}

Then in both screens:

-      final raw = e.toString();
-      final anyhowMatch = RegExp(r'^.*?AnyhowException\((.+)\)$').firstMatch(raw);
-      final msg = anyhowMatch != null ? anyhowMatch.group(1)! : raw;
+      final msg = extractAnyhowMessage(e.toString());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/order/screens/add_lightning_invoice_screen.dart` around lines 93
- 95, Extract the duplicated AnyhowException parsing into a shared utility
function, such as extractAnyhowMessage, preserving the existing regex and
fallback behavior. Update the error handling in both
add_lightning_invoice_screen.dart and take_order_screen.dart to call this
utility instead of duplicating the extraction logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/src/api/orders.rs`:
- Around line 164-182: Same-key concurrent send_invoice attempts can detach or
delete a newer attempt’s pending waiter. Update detach_request_waiter and
remove_pending_request to accept and validate a per-attempt request_id (or
equivalent identity) before mutating the map entry, and pass that identity
through send_invoice timeout, receive-error, and publish-failure paths. Add a
regression test covering overlapping same-key attempts, ensuring the newer
waiter remains active and receives the daemon reply.

In `@specs/004-mostro-p2p-client/contracts/orders.md`:
- Around line 110-122: Update send_invoice to return the documented
TradeNotFound marker when the persisted trade lookup fails, replacing the
free-form “no persisted trade key...” error while preserving the existing
handling for InvalidInvoice and NoDaemonResponse.

---

Nitpick comments:
In `@lib/features/order/screens/add_lightning_invoice_screen.dart`:
- Around line 93-95: Extract the duplicated AnyhowException parsing into a
shared utility function, such as extractAnyhowMessage, preserving the existing
regex and fallback behavior. Update the error handling in both
add_lightning_invoice_screen.dart and take_order_screen.dart to call this
utility instead of duplicating the extraction logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26f2f3ff-1c8d-4a9d-8f5d-06c235f7837f

📥 Commits

Reviewing files that changed from the base of the PR and between b6f78f8 and 31d87e9.

📒 Files selected for processing (17)
  • lib/features/order/screens/add_lightning_invoice_screen.dart
  • lib/features/order/screens/pay_lightning_invoice_screen.dart
  • lib/features/order/screens/take_order_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/orders.rs
  • rust/src/mostro/actions.rs
  • specs/004-mostro-p2p-client/contracts/orders.md

Comment thread rust/src/api/orders.rs
Comment thread specs/004-mostro-p2p-client/contracts/orders.md

@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.

Code Review Summary

Blocking

  • rust/src/api/orders.rs:1638 — The early return for correlated take replies bypasses the status-sync arms below for action-only progression replies. When classify_take_reply() has no payload to extract a status from, take_order() can persist the trade as Pending even though Mostro already advanced it (for example WaitingBuyerInvoice / WaitingSellerToPay).

Looks Good

  • The request-id correlation and timeout handling for take/add-invoice are a solid step up from the previous optimistic flow.

Verdict: Request changes

Comment thread rust/src/api/orders.rs
…t_id

- classify_take_reply falls back to the action-implied status (shared
  status_for_action helper, also used by the status-sync arm) so a take
  whose first reply is action-only no longer persists the trade as Pending
- detach_request_waiter/remove_pending_request now require the attempt's
  request_id: overlapping same-key attempts (send_invoice reuses the take's
  trade key) can no longer cross-detach or delete each other's records;
  subscription-end cleanup stays unconditional as purge_pending_request
- send_invoice returns the documented TradeNotFound marker

@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.

Code Review Summary

Looks Good

  • The earlier take-order status-sync blocker is fixed on the current head.
  • take_order now waits for the daemon reply, classifies action-only progress replies correctly, and persists the real status/amount instead of a phantom pending trade.
  • send_invoice keeps the request-id correlation and status propagation intact, and the focused Rust tests pass on this head.

Verdict: Approve

@Catrya Catrya merged commit b70a6b0 into main Jul 11, 2026
1 check 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.

Take-order / add-invoice flow is fully optimistic: daemon rejections (CantDo) are silently dropped and the app shows a fake trade

1 participant