Skip to content

feat(us10): phase 13 — post-trade rating system - #64

Merged
grunch merged 3 commits into
mainfrom
006-mostro-p2p-client
Mar 30, 2026
Merged

feat(us10): phase 13 — post-trade rating system#64
grunch merged 3 commits into
mainfrom
006-mostro-p2p-client

Conversation

@grunch

@grunch grunch commented Mar 30, 2026

Copy link
Copy Markdown
Member

T094 — rust/src/api/reputation.rs: submit_rating (validates 1–5,
AlreadyRated, PrivacyModeEnabled), get/set_privacy_mode,
get_rating_for_trade, handle_rating_received, RatingStream.
Add RatingInfo + RatingReceivedEvent to types.rs. 7 tests pass.

T095 — lib/features/rate/screens/rate_counterpart_screen.dart:
"RATE" header, double-bolt success indicator, StarRating widget,
"X/5" display, green filled SUBMIT (disabled until rating > 0),
green outline CLOSE (skips rating). Route /rate_user/:orderId wired.

T096 — lib/features/rate/widgets/star_rating.dart:
5 tappable stars, filled = mostroGreen #8CC63F, empty = dark-gray
outline, tap sets 1-based rating, read-only when onChanged is null.

T097 — trade_detail_screen.dart: add pendingRating + rated to
TradeStatus enum; action buttons for each state (RATE→/rate_user,
CLOSE); instruction text for both states. Route no longer stub.

Summary by CodeRabbit

  • New Features
    • Users can rate trading counterparts on a 1–5 star scale after a completed trade.
    • Dedicated rating screen with an intuitive tappable star selector and submit/close actions.
    • Trade details now show rating-related statuses and a RATE action when applicable.
    • Backend reputation support with privacy-mode controls and live rating events.

grunch added 2 commits March 30, 2026 17:47
T094 — rust/src/api/reputation.rs: submit_rating (validates 1–5,
  AlreadyRated, PrivacyModeEnabled), get/set_privacy_mode,
  get_rating_for_trade, handle_rating_received, RatingStream.
  Add RatingInfo + RatingReceivedEvent to types.rs. 7 tests pass.

T095 — lib/features/rate/screens/rate_counterpart_screen.dart:
  "RATE" header, double-bolt success indicator, StarRating widget,
  "X/5" display, green filled SUBMIT (disabled until rating > 0),
  green outline CLOSE (skips rating). Route /rate_user/:orderId wired.

T096 — lib/features/rate/widgets/star_rating.dart:
  5 tappable stars, filled = mostroGreen #8CC63F, empty = dark-gray
  outline, tap sets 1-based rating, read-only when onChanged is null.

T097 — trade_detail_screen.dart: add pendingRating + rated to
  TradeStatus enum; action buttons for each state (RATE→/rate_user,
  CLOSE); instruction text for both states. Route no longer stub.
…tion

- Replace separate contains()/insert() calls with try_insert_if_absent()
  that holds the write lock for the full check-and-insert, preventing
  TOCTOU races on concurrent submit_rating() calls
- Add privacy_lock() test Mutex to serialize tests that mutate the
  global privacy_mode flag, preventing flaky parallel test failures
@coderabbitai

coderabbitai Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c5594aaf-b2eb-4b98-aaea-d99fa7db769d

📥 Commits

Reviewing files that changed from the base of the PR and between 7dc3978 and d675bbe.

📒 Files selected for processing (2)
  • lib/features/rate/widgets/star_rating.dart
  • rust/src/api/reputation.rs
✅ Files skipped from review due to trivial changes (1)
  • rust/src/api/reputation.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/features/rate/widgets/star_rating.dart

Walkthrough

Added a post-trade rating feature: a Flutter rating screen and star widget, route wiring and trade-status UI changes, plus a new Rust in-memory reputation API (ratings storage, privacy toggle, event broadcast) and corresponding types and tests.

Changes

Cohort / File(s) Summary
Routing & UI — Rating
lib/core/app_routes.dart, lib/features/rate/screens/rate_counterpart_screen.dart, lib/features/rate/widgets/star_rating.dart
Replaced stub route /rate_user/:orderId with RateCounterpartScreen(orderId). Added RateCounterpartScreen (stateful, rating selection, submit/close flow) and StarRating (5-star interactive widget, configurable size).
Trade detail UI
lib/features/trades/screens/trade_detail_screen.dart
Added TradeStatus variants pendingRating and rated; updated instruction text and action buttons to surface a "RATE" flow (navigates to rate route) and to hide further rate actions once rated.
Rust reputation backend
rust/src/api/reputation.rs, rust/src/api/types.rs, rust/src/api/mod.rs
Added api::reputation module: in-memory RatingStore (RwLock map), broadcast channel for rating events, privacy-mode flag, public APIs (submit_rating, get/set_privacy_mode, get_rating_for_trade, handle_rating_received, on_rating_received/RatingStream) and new serde types RatingInfo and RatingReceivedEvent. Includes Tokio unit tests.
Spec updates
specs/004-mostro-p2p-client/tasks.md
Marked T094–T097 (reputation API, UI screen, star widget, trade-detail wiring) complete.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Flutter UI
    participant Router as AppRoute
    participant Backend as Rust reputation API
    participant Store as In-Memory Store
    participant Events as Broadcast Channel

    UI->>Router: Navigate to /rate_user/:orderId
    Router->>UI: Instantiates RateCounterpartScreen(orderId)
    UI->>UI: User selects rating via StarRating
    UI->>Backend: submit_rating(trade_id, score)
    Backend->>Store: validate & write local rating (if allowed)
    Store-->>Backend: ack
    Backend->>Events: publish RatingReceivedEvent
    Events-->>Backend: subscribers notified (on_rating_received)
    Backend-->>UI: submission result (success/failure)
    UI->>UI: pop / show snackbar
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hop-hop, I hopped to rate with glee,

Five shiny stars for trade and me.
From fluttered screens to Rusty streams,
I twitched my nose at privacy dreams.
A carrot-toast to ratings done—hip hooray, one, two, three!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing a post-trade rating system (phase 13 of user story 10), which directly aligns with all the changes across Rust backend, Flutter UI, and routing components.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 006-mostro-p2p-client

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 and usage tips.

@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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/rate/screens/rate_counterpart_screen.dart`:
- Around line 36-43: In _submit(), replace the placeholder delay with a call to
the generated Rust bridge function reputation.submit_rating(widget.orderId,
_rating) (or the equivalent FFI wrapper) and await it; only call context.pop()
after that call completes successfully; catch bridge errors and surface them to
the user (e.g., show a snackbar/dialog) so privacy/AlreadyRated errors are
visible; ensure _isSubmitting is cleared in a finally block (setState(() =>
_isSubmitting = false)) and keep the mounted checks before calling context.pop()
or any UI updates.

In `@lib/features/rate/widgets/star_rating.dart`:
- Around line 41-50: Replace the pointer-only GestureDetector with an
interactive IconButton so each star is keyboard-focusable and exposed to
assistive tech: in the widget that builds each star (currently using
GestureDetector with onTap => onChanged!(index + 1)), swap to IconButton, wire
its onPressed to call onChanged!(index + 1) when onChanged is not null, preserve
the Icon parameters (use isFilled to pick Icons.star_rounded or
Icons.star_outline_rounded, color from filledColor/emptyColor, and size from
starSize), and add a semantic label (e.g., "Rate X out of Y" or "Select
{index+1} stars") via the IconButton's tooltip or by wrapping with Semantics so
screen readers get per-star descriptions.

In `@rust/src/api/reputation.rs`:
- Around line 95-117: submit_rating currently accepts any trade_id and only
writes local state; change it to first validate the trade is complete by looking
up the session/trade (use your session/trade lookup helper to obtain the trade
key and counterparty Mostro pubkey), ensure privacy_mode is still false, then
publish a RateUser MostroMessage wrapped via NIP-59 Gift Wrap to the
counterparty and await confirmation/success; only after the publish succeeds
should you call rating_store() and invoke try_insert_if_absent with the
RatingInfo to persist locally, and on publish failure return an error without
persisting. Ensure errors from the publish path are propagated and that the
sequence prevents TOCTOU races by re-checking privacy_mode/trade completion
immediately before persistence.
- Around line 22-63: The current RatingStore uses ratings: HashMap<String,
RatingInfo> which only allows one RatingInfo per trade_id and causes
races/overwrites between the two parties; change the storage to represent both
sides (e.g. replace RatingInfo value with a small struct TradeRatings { mine:
Option<RatingInfo>, peer: Option<RatingInfo> } or switch keys to (trade_id,
is_mine)) and update RatingStore methods accordingly: modify get to return the
appropriate side(s), update try_insert_if_absent and insert to insert into the
correct slot (mine vs peer) instead of rejecting/overwriting, and adjust any
callsites such as submit_rating and handle_rating_received to indicate which
side the rating belongs to so the store can populate the correct field. Ensure
event_tx semantics remain unchanged and that atomic locking still protects the
composite TradeRatings entry.
- Around line 150-177: handle_rating_received currently records incoming ratings
and emits RatingReceivedEvent regardless of privacy mode; update it to check the
privacy flag (use whatever module-level function/variable that indicates privacy
mode, e.g., privacy_mode_enabled() or is_privacy_mode()) at the start and return
early when privacy is enabled. Specifically, in handle_rating_received, if
privacy mode is active skip the store.insert(...) call and do not send
store.event_tx.send(event) — simply return Ok(()). Keep the rest of validation
(score range) intact and reference RatingReceivedEvent, RatingInfo,
rating_store(), and unix_now() when applying the guard.
🪄 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: 668acb6e-10a4-4a2f-9328-8af7029faf55

📥 Commits

Reviewing files that changed from the base of the PR and between 4c2b410 and 7dc3978.

📒 Files selected for processing (8)
  • lib/core/app_routes.dart
  • lib/features/rate/screens/rate_counterpart_screen.dart
  • lib/features/rate/widgets/star_rating.dart
  • lib/features/trades/screens/trade_detail_screen.dart
  • rust/src/api/mod.rs
  • rust/src/api/reputation.rs
  • rust/src/api/types.rs
  • specs/004-mostro-p2p-client/tasks.md

Comment on lines +36 to +43
Future<void> _submit() async {
if (_rating == 0) return;
setState(() => _isSubmitting = true);
try {
// TODO(bridge): Call reputation.submit_rating(widget.orderId, _rating)
// via Rust bridge once FFI bindings are generated.
await Future.delayed(const Duration(milliseconds: 300));
if (mounted) context.pop();

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.

⚠️ Potential issue | 🔴 Critical

SUBMIT currently reports success without saving anything.

This handler only waits 300 ms and pops the route; it never calls rust/src/api/reputation.rs::submit_rating(). The UI therefore shows a successful flow while nothing is persisted, and backend errors like PrivacyModeEnabled / AlreadyRated can never reach the user. Wire the generated bridge call here and dismiss only after it succeeds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/rate/screens/rate_counterpart_screen.dart` around lines 36 - 43,
In _submit(), replace the placeholder delay with a call to the generated Rust
bridge function reputation.submit_rating(widget.orderId, _rating) (or the
equivalent FFI wrapper) and await it; only call context.pop() after that call
completes successfully; catch bridge errors and surface them to the user (e.g.,
show a snackbar/dialog) so privacy/AlreadyRated errors are visible; ensure
_isSubmitting is cleared in a finally block (setState(() => _isSubmitting =
false)) and keep the mounted checks before calling context.pop() or any UI
updates.

Comment thread lib/features/rate/widgets/star_rating.dart Outdated
Comment thread rust/src/api/reputation.rs
Comment on lines +95 to +117
pub async fn submit_rating(trade_id: String, score: u8) -> Result<()> {
if score < 1 || score > 5 {
bail!("InvalidScore: score must be between 1 and 5, got {score}");
}

let store = rating_store();

if store.privacy_mode.load(Ordering::SeqCst) {
bail!("PrivacyModeEnabled: cannot submit rating while privacy mode is active");
}

// TODO(Phase 14+): Look up the session to get trade key + mostro pubkey,
// then send a RateUser MostroMessage via NIP-59 Gift Wrap.

// Atomic check-and-insert under write lock — prevents TOCTOU races.
store
.try_insert_if_absent(RatingInfo {
trade_id,
score,
is_mine: true,
created_at: unix_now(),
})
.await?;

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.

⚠️ Potential issue | 🔴 Critical

submit_rating accepts arbitrary trades and never publishes the rating.

The only enforced preconditions here are score range and privacy mode. That lets callers rate unfinished/nonexistent trade_ids, and the success path only writes to local in-memory state—no RateUser message is sent, so counterpart reputation cannot update outside this process or survive restart. Validate that the trade is complete, publish the protocol message, and only persist the rating after that succeeds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/api/reputation.rs` around lines 95 - 117, submit_rating currently
accepts any trade_id and only writes local state; change it to first validate
the trade is complete by looking up the session/trade (use your session/trade
lookup helper to obtain the trade key and counterparty Mostro pubkey), ensure
privacy_mode is still false, then publish a RateUser MostroMessage wrapped via
NIP-59 Gift Wrap to the counterparty and await confirmation/success; only after
the publish succeeds should you call rating_store() and invoke
try_insert_if_absent with the RatingInfo to persist locally, and on publish
failure return an error without persisting. Ensure errors from the publish path
are propagated and that the sequence prevents TOCTOU races by re-checking
privacy_mode/trade completion immediately before persistence.

Comment thread rust/src/api/reputation.rs
…ibility

rust/src/api/reputation.rs:
- Replace HashMap<String,RatingInfo> with HashMap<String,TradeRatings>
  so mine/peer ratings for the same trade coexist without overwriting;
  try_insert_mine() guards only the local-user slot, insert_peer() upserts
  the peer slot independently
- Add privacy mode guard to handle_rating_received: incoming ratings are
  silently discarded when privacy mode is active
- Add 2 new tests: privacy discard and mine+peer coexistence

lib/features/rate/widgets/star_rating.dart:
- Replace GestureDetector with IconButton so each star is keyboard-
  focusable and exposed to assistive tech; tooltip provides per-star
  semantic label ("Select N stars")
@grunch
grunch merged commit 6b4344c into main Mar 30, 2026
1 check passed
@grunch
grunch deleted the 006-mostro-p2p-client branch March 30, 2026 21:34
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.

1 participant