Skip to content

refactor DM routing and add live subscription updates#47

Open
arkanoider wants to merge 20 commits intomainfrom
single-dm-router
Open

refactor DM routing and add live subscription updates#47
arkanoider wants to merge 20 commits intomainfrom
single-dm-router

Conversation

@arkanoider
Copy link
Collaborator

@arkanoider arkanoider commented Mar 23, 2026

Summary

  • centralize DM handling so wait_for_dm registers waiters on the router instead of creating separate raw-notification subscriptions
  • migrate order tracking commands to router-based TrackOrder flow and wire router sender initialization through startup and reload paths
  • add live order/dispute subscriptions in fetch scheduler and keep 30s reconciliation polling as a safety net

Summary by CodeRabbit

  • New Features

    • Enter-confirmation with visual YES/NO buttons for order creation
    • Scrollable invoice view with ↑/↓ and page navigation
    • Background DM router for real-time trade message delivery and tracking
    • Improved order form navigation and contextual field help
  • Bug Fixes

    • Prevented invoice UI being replaced after taking orders (race)
    • Fixed “Invalid date” display for orders with missing timestamps
    • More robust live order/dispute reconciliation and tracking
    • More reliable clipboard copying on Linux
  • Documentation

    • Updated message flow, startup, and database docs for DM routing and maker/taker semantics

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Introduces a DM router and subscription command channel for real‑time trade DM handling, replaces periodic DM polling with an event-driven router, distinguishes maker/taker in order persistence, adds FormField-driven UI navigation and YES/NO button state, and wires dm_subscription channels through startup, key handlers, and async order flows.

Changes

Cohort / File(s) Summary
Dependencies
Cargo.toml
Added serde_json and libc dependencies.
Docs
docs/MESSAGE_FLOW_AND_PROTOCOL.md, docs/DATABASE.md, docs/STARTUP_AND_CONFIG.md
Documented DM router flow, maker/taker semantics for is_mine, and DM router startup wiring.
DM Router & Message Loop
src/util/dm_utils/mod.rs, src/util/dm_utils/dm_helpers.rs, src/util/dm_utils/notifications_ch_mng.rs, src/util/dm_utils/order_ch_mng.rs
Added DmRouterCmd (TrackOrder/RegisterWaiter), global router Tx setter, refactored wait_for_dm to register waiters via router, rewrote listen_for_order_messages into an event-driven router with subscription management, added decrypt/cache logic and trade-DM handling, renamed handle_order_resulthandle_operation_result.
Main wiring & Scheduler
src/main.rs, src/util/order_utils/fetch_scheduler.rs
Wired dm_subscription_tx/rx into app channels and background listener; registered DM router Tx at startup; start_fetch_scheduler now accepts settings and scheduler loops refactored to reconciliation ticks + relay event handling.
Order persistence & models
src/models.rs, src/util/db_utils.rs, tests/db_tests.rs
Order::new gains is_maker; added upsert_from_small_order_dm and update_status; updated save_order signature and tests to pass maker/taker flag; added helpers to refresh/update local status.
Order send/take flows
src/util/order_utils/send_new_order.rs, src/util/order_utils/take_order.rs, src/util/order_utils/execute_*
send_new_order/take_order accept optional dm_subscription_tx, register TrackOrder early, removed client arg from wait_for_dm call sites, ensure/normalize order IDs before persistence, and pass maker/taker flag to save_order.
Order helpers & status mapping
src/util/order_utils/helper.rs, src/util/order_utils/mod.rs
Added inferred_status_from_trade_action and map_action_to_status and re-exported them.
UI state & form model
src/ui/user_state.rs, src/ui/orders.rs, src/ui/app_state.rs
ConfirmingOrder variant becomes struct with selected_button; introduced FormField enum and FormState::new_default_form(); added InvoiceInputState.scroll_y; added pending_post_take_operation_result to AppState.
UI rendering & helpers
src/ui/order_confirm.rs, src/ui/order_form.rs, src/ui/helpers.rs, src/ui/message_notification.rs, src/ui/operation_result.rs, src/ui/exit_confirm.rs, src/ui/tabs/orders_tab.rs, src/ui/draw.rs
Refactored order confirmation to accept selected_button and use render_yes_no_buttons; reworked form layout and field help panel; invoice display supports vertical scrolling; operation-result layout bounded; made created_at display null-safe.
Key handling & async tasks
src/ui/key_handler/mod.rs, src/ui/key_handler/async_tasks.rs, src/ui/key_handler/*
Threaded dm_subscription_tx through EnterKeyContext and async tasks; updated key handlers to use FormField, support invoice scroll keys, defer operation results in notifications, and handle DM router registration on key reload.
Misc & tests
tests/db_tests.rs, .gitignore
Updated test call sites for Order::new signature; added reports/ to .gitignore.

Sequence Diagram(s)

sequenceDiagram
    participant UI as "UI"
    participant Send as "send_new_order / take_order"
    participant Router as "DM Router (dm_subscription)"
    participant Mostro as "Mostro Relay"
    participant BG as "Background listener"
    participant DB as "Database"

    UI->>Send: user confirms (Enter YES)
    Send->>Router: RegisterWaiter / TrackOrder (trade_keys / order_id)
    Router-->>Send: ack register
    Send->>Mostro: send DM to Mostro relay
    Mostro-->>Router: GiftWrap event (relay)
    Router->>BG: dispatch event to waiter(s) or tracked order handler
    BG->>DB: upsert/update order status
    DB-->>BG: persist result
    BG->>UI: emit notification or stash pending_post_take_operation_result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • grunch
  • AndreaDiazCorreia
  • Catrya

Poem

🐰 I tunneled routes through DM streams tonight,

Buttons now bob left or right in soft light.
Invoices scroll and trade keys take to flight,
Maker or taker, every hop feels right.
🥕📬

🚥 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 PR title 'refactor DM routing and add live subscription updates' directly and clearly describes the main changes: centralized DM routing with a shared router and new live subscription updates for orders/disputes.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch single-dm-router

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (9)
tests/db_tests.rs (1)

174-183: Tests updated correctly. Consider adding taker coverage.

The signature updates are correct. All tests currently use is_maker: true. Consider adding a test case with is_maker: false to verify taker order storage behavior works as expected.

,

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

In `@tests/db_tests.rs` around lines 174 - 183, Add a complementary test that
exercises the taker path by calling Order::new with is_maker: false (e.g.,
create a new variable like order_taker using the same small_order or a clone
with modified fields), await and unwrap the result, then assert the expected
taker-specific storage/behavior (matching IDs, amounts, and any taker-specific
columns) just as the existing maker assertions do; ensure you use the same
helper variables (small_order, trade_keys, pool) and change only the is_maker
boolean to false so the test verifies taker order storage and retrieval.
debug-notes.md (1)

1-62: Consider removing debug notes before merging to main.

This file contains internal debugging handoff notes specific to a development investigation. While valuable during development, it should likely not be committed to the main branch as it:

  1. References a specific debug branch (fix-windows-launch) that may not match the PR context
  2. Contains ephemeral debugging instructions ("What to test next (first thing tomorrow)")
  3. Documents internal implementation investigation rather than user-facing or architectural documentation

Consider either removing this file before merge, or converting actionable insights into proper documentation (e.g., a troubleshooting section in docs, or comments in the relevant source files).

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

In `@debug-notes.md` around lines 1 - 62, This debug-only markdown
(debug-notes.md) should not be merged as-is; either delete the file from the PR
or move the useful, non-ephemeral parts into a proper place (e.g., an internal
troubleshooting doc or an issue) and remove the step-by-step debug checklist and
branch-specific references; specifically, remove the transient "What to test
next" and branch name mentions or convert them into durable docs, then update
the PR to remove debug-notes.md or replace it with a concise, sanitized
troubleshooting entry.
src/util/order_utils/take_order.rs (1)

143-183: Consider documenting the dual TrackOrder sends.

The code sends TrackOrder twice: once early (lines 75-87) and once after DB save (lines 172-182). While this redundancy is likely intentional for robustness, a brief comment explaining why both are needed would help future maintainers understand this isn't accidental duplication.

📝 Suggested comment
                             if let Some(tx) = dm_subscription_tx {
+                                // Re-send TrackOrder after DB save to ensure subscription is active
+                                // even if the early send (before Mostro message) arrived before
+                                // router initialization completed.
                                 log::info!(
                                     "[take_order] Sending DM subscription command for order_id={}, trade_index={}",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/order_utils/take_order.rs` around lines 143 - 183, The code
currently sends OrderDmSubscriptionCmd::TrackOrder twice via dm_subscription_tx
(once earlier and again after save_order) which looks like duplication; add a
short clarifying comment next to the first dm_subscription_tx.send call and the
second send (the block that logs "[take_order] Sending DM subscription
command..." and the earlier send block) stating that the first send is an
optimistic/early tracking signal and the second is a post-persistence
confirmation (to ensure DM tracking even if save fails or to reconcile state
after DB persistence), and reference the surrounding functions/values
(TrackOrder, dm_subscription_tx, save_order, normalized_order,
create_order_result_success) so future maintainers understand this intentional
redundancy.
src/util/order_utils/fetch_scheduler.rs (3)

69-90: Same mutex unwrap() concern applies here.

Line 72 uses .unwrap() on the mutex lock. Apply the same defensive handling as suggested for apply_live_order_update.

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

In `@src/util/order_utils/fetch_scheduler.rs` around lines 69 - 90, In
apply_live_dispute_update, avoid directly calling .unwrap() on the Mutex lock
for disputes; instead handle a poisoned lock like in apply_live_order_update by
matching the result of disputes.lock() (or using .lock().map_err/unwrap_or_else)
to either acquire the guard or recover/log and return early if poisoning occurs,
then proceed to find/update/push the Dispute and sort; ensure you reference the
disputes: Arc<Mutex<Vec<Dispute>>> parameter, the local disputes_lock guard, and
keep the existing log::debug call after a successful update.

34-67: Consider handling poisoned mutex instead of unwrap().

Line 38 uses .unwrap() on the mutex lock, which will panic if the mutex is poisoned (e.g., a previous holder panicked). While mutex poisoning is rare, a panic here would crash the entire order update loop.

Consider using .lock().ok() with an early return or logging, consistent with how lock failures are handled elsewhere in this file (e.g., lines 151-160).

♻️ Suggested fix
 fn apply_live_order_update(orders: &Arc<Mutex<Vec<SmallOrder>>>, order: SmallOrder) {
     let Some(order_id) = order.id else {
         return;
     };
-    let mut orders_lock = orders.lock().unwrap();
+    let mut orders_lock = match orders.lock() {
+        Ok(guard) => guard,
+        Err(e) => {
+            log::warn!("[orders_live] Failed to lock orders: {}", e);
+            return;
+        }
+    };
     if order.status != Some(Status::Pending) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/order_utils/fetch_scheduler.rs` around lines 34 - 67, The mutex lock
in apply_live_order_update currently uses orders.lock().unwrap(), which can
panic on a poisoned mutex; change it to handle the PoisonError by replacing the
unwrap with a non-panicking pattern (e.g., match or .lock().ok() / .map_err())
that logs the error and returns early on failure; locate apply_live_order_update
and replace the unwrap call with the same poisoned-mutex handling pattern used
elsewhere in this file (log the poisoning context and return) so the
order-update loop does not crash on a poisoned mutex.

200-215: Reconciliation loops also use unwrap() on mutex locks.

Lines 208 and 299 use .unwrap() on mutex locks inside the reconciliation tick handlers. While the outer lock acquisition at lines 189-198 and 286-295 properly handles errors with continue, these inner locks could still panic.

Consider applying consistent error handling for all mutex acquisitions.

Also applies to: 296-306

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

In `@src/util/order_utils/fetch_scheduler.rs` around lines 200 - 215, The code
currently calls orders_clone.lock().unwrap() inside the reconciliation handlers
(e.g., where get_orders is awaited and the subsequent
orders_lock.clear()/extend()/len() are used), which can panic on a poisoned
mutex; replace those unwrap() calls with proper error handling (e.g., match or
if let Ok(mut orders_lock) = orders_clone.lock() { ... } else {
log::warn/error!("failed to acquire orders mutex: {:?}", err); continue; }) so
the handler skips this tick on lock acquisition failure instead of panicking;
apply the same change to the other reconciliation site that also uses
orders_clone.lock().unwrap() (lines referenced in the review).
src/util/dm_utils/mod.rs (3)

711-722: Stale waiters may linger until the next GiftWrap event.

When a waiter's oneshot receiver times out (in wait_for_dm at line 171-174), the corresponding PendingDmWaiter remains in pending_waiters until the next GiftWrap event triggers the drain logic. This isn't a memory leak since eventually all waiters will be checked and removed, but it could be cleaner.

Consider periodically pruning waiters whose response_tx.is_closed() returns true, or accepting this minor inefficiency given the expected low waiter count.

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

In `@src/util/dm_utils/mod.rs` around lines 711 - 722, The pending_waiters vector
can retain entries whose oneshot receivers have timed out; update the GiftWrap
handling in the block around pending_waiters (where PendingDmWaiter is used and
nip59::extract_rumor(...) is called) to prune closed waiters: before attempting
extract_rumor, filter or drain out any waiter where
waiter.response_tx.is_closed() (removing them without processing), then proceed
to try matching remaining waiters and rebuild pending_waiters from non-matching
ones; this ensures waiters removed by wait_for_dm timeouts are dropped promptly
rather than waiting for the next event.

142-179: Unused _client parameter in wait_for_dm.

The _client parameter (line 145) is now unused since the function delegates to the DM router instead of subscribing directly. Consider removing it if the public API can be changed, or document why it's retained for compatibility.


528-547: Fallback resolver iterates all active orders.

resolve_order_for_event tries to decrypt the event with each active order's trade keys. This is O(n) decryption attempts per unmatched event. For a typical user with few concurrent orders, this is acceptable. If order counts grow significantly, consider caching pubkey-to-order mappings.

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

In `@src/util/dm_utils/mod.rs` around lines 528 - 547, resolve_order_for_event
currently tries every active order (active_order_trade_indices) and calls
user.derive_trade_keys + nip59::extract_rumor for each, causing O(n) decrypt
attempts; replace this fallback with a cache mapping the trade pubkey to its
order metadata so you can attempt decryption only for matching keys. Implement a
shared cache (e.g., HashMap<PublicKey, (Uuid, i64, Keys)> or similar) that is
updated when orders are added/removed, look up the event's sender/recipient
pubkey first and only call nip59::extract_rumor for the matched entry, and fall
back to the original loop only if the cache lookup misses; update
resolve_order_for_event to consult that cache instead of blindly iterating
active_order_trade_indices and keep existing variable names
(resolve_order_for_event, active_order_trade_indices, user.derive_trade_keys,
nip59::extract_rumor) to locate where to integrate the cache.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ui/key_handler/form_input.rs`:
- Around line 32-42: The match on form.focused can panic when
FormField::FiatAmountMax is focused but form.use_range is false; change the
FiatAmountMax arm in the target-selection match in form_input.rs to handle both
cases (e.g. FormField::FiatAmountMax => if form.use_range { &mut
form.fiat_amount_max } else { &mut form.fiat_amount }) so it never falls through
to unreachable!, and apply the same conditional pattern to the corresponding
match in handle_backspace (use the same FormField::FiatAmountMax conditional
mapping to avoid the panic).

In `@src/ui/tabs/orders_tab.rs`:
- Around line 117-125: The code coerces missing timestamps via
order.created_at.unwrap_or(0) so DateTime::from_timestamp(0, 0) yields the Unix
epoch instead of showing the "Invalid date" fallback; update the chain to
propagate None by using and_then on order.created_at and only call
DateTime::from_timestamp when Some(value) (e.g., change the creation of date to
something like order.created_at.and_then(|ts| DateTime::from_timestamp_opt(ts,
0) or equivalent) and then map/with_timezone/format into the Cell::from closure
used for date_cell) so missing created_at results in the unwrap_or_else("Invalid
date") path.

---

Nitpick comments:
In `@debug-notes.md`:
- Around line 1-62: This debug-only markdown (debug-notes.md) should not be
merged as-is; either delete the file from the PR or move the useful,
non-ephemeral parts into a proper place (e.g., an internal troubleshooting doc
or an issue) and remove the step-by-step debug checklist and branch-specific
references; specifically, remove the transient "What to test next" and branch
name mentions or convert them into durable docs, then update the PR to remove
debug-notes.md or replace it with a concise, sanitized troubleshooting entry.

In `@src/util/dm_utils/mod.rs`:
- Around line 711-722: The pending_waiters vector can retain entries whose
oneshot receivers have timed out; update the GiftWrap handling in the block
around pending_waiters (where PendingDmWaiter is used and
nip59::extract_rumor(...) is called) to prune closed waiters: before attempting
extract_rumor, filter or drain out any waiter where
waiter.response_tx.is_closed() (removing them without processing), then proceed
to try matching remaining waiters and rebuild pending_waiters from non-matching
ones; this ensures waiters removed by wait_for_dm timeouts are dropped promptly
rather than waiting for the next event.
- Around line 528-547: resolve_order_for_event currently tries every active
order (active_order_trade_indices) and calls user.derive_trade_keys +
nip59::extract_rumor for each, causing O(n) decrypt attempts; replace this
fallback with a cache mapping the trade pubkey to its order metadata so you can
attempt decryption only for matching keys. Implement a shared cache (e.g.,
HashMap<PublicKey, (Uuid, i64, Keys)> or similar) that is updated when orders
are added/removed, look up the event's sender/recipient pubkey first and only
call nip59::extract_rumor for the matched entry, and fall back to the original
loop only if the cache lookup misses; update resolve_order_for_event to consult
that cache instead of blindly iterating active_order_trade_indices and keep
existing variable names (resolve_order_for_event, active_order_trade_indices,
user.derive_trade_keys, nip59::extract_rumor) to locate where to integrate the
cache.

In `@src/util/order_utils/fetch_scheduler.rs`:
- Around line 69-90: In apply_live_dispute_update, avoid directly calling
.unwrap() on the Mutex lock for disputes; instead handle a poisoned lock like in
apply_live_order_update by matching the result of disputes.lock() (or using
.lock().map_err/unwrap_or_else) to either acquire the guard or recover/log and
return early if poisoning occurs, then proceed to find/update/push the Dispute
and sort; ensure you reference the disputes: Arc<Mutex<Vec<Dispute>>> parameter,
the local disputes_lock guard, and keep the existing log::debug call after a
successful update.
- Around line 34-67: The mutex lock in apply_live_order_update currently uses
orders.lock().unwrap(), which can panic on a poisoned mutex; change it to handle
the PoisonError by replacing the unwrap with a non-panicking pattern (e.g.,
match or .lock().ok() / .map_err()) that logs the error and returns early on
failure; locate apply_live_order_update and replace the unwrap call with the
same poisoned-mutex handling pattern used elsewhere in this file (log the
poisoning context and return) so the order-update loop does not crash on a
poisoned mutex.
- Around line 200-215: The code currently calls orders_clone.lock().unwrap()
inside the reconciliation handlers (e.g., where get_orders is awaited and the
subsequent orders_lock.clear()/extend()/len() are used), which can panic on a
poisoned mutex; replace those unwrap() calls with proper error handling (e.g.,
match or if let Ok(mut orders_lock) = orders_clone.lock() { ... } else {
log::warn/error!("failed to acquire orders mutex: {:?}", err); continue; }) so
the handler skips this tick on lock acquisition failure instead of panicking;
apply the same change to the other reconciliation site that also uses
orders_clone.lock().unwrap() (lines referenced in the review).

In `@src/util/order_utils/take_order.rs`:
- Around line 143-183: The code currently sends
OrderDmSubscriptionCmd::TrackOrder twice via dm_subscription_tx (once earlier
and again after save_order) which looks like duplication; add a short clarifying
comment next to the first dm_subscription_tx.send call and the second send (the
block that logs "[take_order] Sending DM subscription command..." and the
earlier send block) stating that the first send is an optimistic/early tracking
signal and the second is a post-persistence confirmation (to ensure DM tracking
even if save fails or to reconcile state after DB persistence), and reference
the surrounding functions/values (TrackOrder, dm_subscription_tx, save_order,
normalized_order, create_order_result_success) so future maintainers understand
this intentional redundancy.

In `@tests/db_tests.rs`:
- Around line 174-183: Add a complementary test that exercises the taker path by
calling Order::new with is_maker: false (e.g., create a new variable like
order_taker using the same small_order or a clone with modified fields), await
and unwrap the result, then assert the expected taker-specific storage/behavior
(matching IDs, amounts, and any taker-specific columns) just as the existing
maker assertions do; ensure you use the same helper variables (small_order,
trade_keys, pool) and change only the is_maker boolean to false so the test
verifies taker order storage and retrieval.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b91e051d-fbf9-4a01-8f5d-d35955bae7a8

📥 Commits

Reviewing files that changed from the base of the PR and between c899ed8 and ef5dba7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (36)
  • Cargo.toml
  • debug-notes.md
  • docs/MESSAGE_FLOW_AND_PROTOCOL.md
  • src/main.rs
  • src/models.rs
  • src/ui/app_state.rs
  • src/ui/draw.rs
  • src/ui/exit_confirm.rs
  • src/ui/helpers.rs
  • src/ui/key_handler/async_tasks.rs
  • src/ui/key_handler/confirmation.rs
  • src/ui/key_handler/enter_handlers.rs
  • src/ui/key_handler/esc_handlers.rs
  • src/ui/key_handler/form_input.rs
  • src/ui/key_handler/message_handlers.rs
  • src/ui/key_handler/mod.rs
  • src/ui/key_handler/navigation.rs
  • src/ui/key_handler/user_handlers.rs
  • src/ui/message_notification.rs
  • src/ui/operation_result.rs
  • src/ui/order_confirm.rs
  • src/ui/order_form.rs
  • src/ui/orders.rs
  • src/ui/tabs/orders_tab.rs
  • src/ui/user_state.rs
  • src/util/db_utils.rs
  • src/util/dm_utils/mod.rs
  • src/util/dm_utils/notifications_ch_mng.rs
  • src/util/dm_utils/order_ch_mng.rs
  • src/util/mod.rs
  • src/util/order_utils/fetch_scheduler.rs
  • src/util/order_utils/helper.rs
  • src/util/order_utils/mod.rs
  • src/util/order_utils/send_new_order.rs
  • src/util/order_utils/take_order.rs
  • tests/db_tests.rs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/util/dm_utils/mod.rs (2)

46-52: Consider returning a success indicator from set_dm_router_cmd_tx.

If the lock is poisoned, the function logs a warning but returns normally, leaving the caller unaware that the router sender wasn't set. This could cause silent failures downstream when wait_for_dm attempts to register waiters.

Proposed change
-pub fn set_dm_router_cmd_tx(tx: mpsc::UnboundedSender<DmRouterCmd>) {
-    if let Ok(mut guard) = DM_ROUTER_CMD_TX.lock() {
-        *guard = Some(tx);
-    } else {
-        log::warn!("[dm_listener] Failed to set DM router sender due to poisoned lock");
-    }
+pub fn set_dm_router_cmd_tx(tx: mpsc::UnboundedSender<DmRouterCmd>) -> bool {
+    match DM_ROUTER_CMD_TX.lock() {
+        Ok(mut guard) => {
+            *guard = Some(tx);
+            true
+        }
+        Err(_) => {
+            log::warn!("[dm_listener] Failed to set DM router sender due to poisoned lock");
+            false
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/dm_utils/mod.rs` around lines 46 - 52, The function
set_dm_router_cmd_tx currently swallows a poisoned lock and only logs a warning;
change its signature to return a Result<(), LockError> or a bool success
indicator so callers can react when the sender wasn't set. Inside
set_dm_router_cmd_tx, return Ok(()) (or true) when the lock is acquired and the
sender is set, and return Err(...) (or false) when DM_ROUTER_CMD_TX.lock()
fails/is poisoned after logging the warning; update call sites such as
wait_for_dm to check the return value and handle the failure path (e.g.,
propagate the error or fail registration) so failures to set the router sender
are not silent.

527-546: Fallback decryption path has O(n) complexity over active orders.

This is acceptable as a fallback for unknown subscription_ids, but if the number of concurrent active orders grows large, the linear decrypt-attempt loop could impact performance. Consider logging metrics here to monitor if this path is hit frequently in production.

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

In `@src/util/dm_utils/mod.rs` around lines 527 - 546, The fallback linear decrypt
loop in resolve_order_for_event (iterating active_order_trade_indices and
calling nip59::extract_rumor) can be expensive at scale — instrument it: add a
metrics counter (e.g., fallback_decrypt_attempts_total) incremented each time
this fallback path is entered, a gauge or histogram for the number of active
orders scanned and the duration of the loop, and emit a low-volume trace/warn
log with the scanned count and duration when the loop runs (and optionally when
a match is found). Place the metric increments and timing around the section
that locks and clones active_order_trade_indices and around the for-loop in
resolve_order_for_event, using your project’s existing metrics/tracing utilities
so you can monitor how often this O(n) path is exercised in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/util/dm_utils/dm_helpers.rs`:
- Around line 74-83: The function ensure_order_giftwrap_subscription currently
violates clippy::too_many_arguments (8 args); fix this by grouping related
parameters (e.g., subscribed_pubkeys and subscription_to_order into a
SubscriptionsContext struct, and pubkey/order_id/trade_index into an OrderKey or
OrderContext) and update the function signature to accept the new context
structs instead of the individual params, then adjust call sites accordingly; if
refactoring call sites is too disruptive, alternatively add
#[allow(clippy::too_many_arguments)] directly above the
ensure_order_giftwrap_subscription declaration to suppress the lint temporarily.

---

Nitpick comments:
In `@src/util/dm_utils/mod.rs`:
- Around line 46-52: The function set_dm_router_cmd_tx currently swallows a
poisoned lock and only logs a warning; change its signature to return a
Result<(), LockError> or a bool success indicator so callers can react when the
sender wasn't set. Inside set_dm_router_cmd_tx, return Ok(()) (or true) when the
lock is acquired and the sender is set, and return Err(...) (or false) when
DM_ROUTER_CMD_TX.lock() fails/is poisoned after logging the warning; update call
sites such as wait_for_dm to check the return value and handle the failure path
(e.g., propagate the error or fail registration) so failures to set the router
sender are not silent.
- Around line 527-546: The fallback linear decrypt loop in
resolve_order_for_event (iterating active_order_trade_indices and calling
nip59::extract_rumor) can be expensive at scale — instrument it: add a metrics
counter (e.g., fallback_decrypt_attempts_total) incremented each time this
fallback path is entered, a gauge or histogram for the number of active orders
scanned and the duration of the loop, and emit a low-volume trace/warn log with
the scanned count and duration when the loop runs (and optionally when a match
is found). Place the metric increments and timing around the section that locks
and clones active_order_trade_indices and around the for-loop in
resolve_order_for_event, using your project’s existing metrics/tracing utilities
so you can monitor how often this O(n) path is exercised in production.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3cce05ea-5f04-4ec6-94ec-806bf0178234

📥 Commits

Reviewing files that changed from the base of the PR and between ef5dba7 and 927b2a1.

📒 Files selected for processing (5)
  • docs/DATABASE.md
  • docs/MESSAGE_FLOW_AND_PROTOCOL.md
  • docs/STARTUP_AND_CONFIG.md
  • src/util/dm_utils/dm_helpers.rs
  • src/util/dm_utils/mod.rs
✅ Files skipped from review due to trivial changes (2)
  • docs/DATABASE.md
  • docs/STARTUP_AND_CONFIG.md

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/util/order_utils/fetch_scheduler.rs (2)

228-235: Inconsistent mutex error handling: unwrap() vs graceful handling.

The reconciliation branch uses .unwrap() on the mutex lock (lines 228, 319), which will panic if the mutex is poisoned. However, apply_live_order_update and apply_live_dispute_update gracefully handle poisoned mutexes by logging and returning early.

Consider handling the poisoned mutex consistently to avoid potential panics:

Proposed fix
                     if let Ok(fetched_orders) = get_orders(
                         &client_for_orders,
                         mostro_pubkey_for_orders,
                         Some(Status::Pending),
                         Some(currencies),
                     )
                     .await
                     {
-                        let mut orders_lock = orders_clone.lock().unwrap();
+                        let mut orders_lock = match orders_clone.lock() {
+                            Ok(guard) => guard,
+                            Err(e) => {
+                                log::warn!("[orders_reconcile] Failed to lock orders: {}", e);
+                                continue;
+                            }
+                        };
                         orders_lock.clear();
                         orders_lock.extend(fetched_orders);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/order_utils/fetch_scheduler.rs` around lines 228 - 235, The
reconciliation branch currently uses orders_clone.lock().unwrap() which will
panic on a poisoned mutex; match the pattern used in apply_live_order_update and
apply_live_dispute_update by handling the Result from orders_clone.lock()
instead of unwrapping: attempt to lock, log an error if Err (including context
like "orders_reconcile: failed to lock orders mutex") and return early,
otherwise proceed to clear/extend the Vec and log the count; update both places
using unwrap() (e.g., where orders_lock is acquired) to use this non-panicking,
logged handling.

167-167: Misleading variable name: latest_settings is actually a snapshot.

The variable latest_settings is cloned once at task spawn time and never refreshed. Currency filter changes won't take effect until the scheduler restarts (e.g., after a key reload). Consider renaming to initial_settings or settings_snapshot for clarity.

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

In `@src/util/order_utils/fetch_scheduler.rs` at line 167, The variable
latest_settings is misleading because it's cloned once at task spawn and never
refreshed; rename it to settings_snapshot or initial_settings to reflect it's a
single snapshot taken at spawn time and update all references (e.g., the cloned
binding currently named latest_settings inside the fetch scheduler task/closure)
accordingly so callers/readers understand currency filter changes won't be
applied until restart; ensure variable rename is applied consistently where
latest_settings is used within the scheduler logic (task spawn/closure) to avoid
confusion.
src/util/dm_utils/mod.rs (3)

881-897: Inconsistent indentation on dispatch_giftwrap_batch call.

The call is indented one level deeper than the surrounding code. This appears to be a formatting inconsistency.

Suggested fix
                         if !parsed_messages.is_empty() {
                             log::info!(
                                 "[dm_listener] Routed GiftWrap by active-order key for unknown subscription_id={} to order_id={}, trade_index={}",
                                 subscription_id,
                                 order_id,
                                 trade_index
                             );
-                                dispatch_giftwrap_batch(
+                            dispatch_giftwrap_batch(
                                 parsed_messages,
                                 ...
-                                )
-                                .await;
+                            )
+                            .await;
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/dm_utils/mod.rs` around lines 881 - 897, The call to
dispatch_giftwrap_batch is indented one level deeper than surrounding code
causing inconsistent formatting; adjust the indentation of the entire call
(starting at dispatch_giftwrap_batch(...) through .await;) to align with the
surrounding block indentation so it matches neighboring statements, referencing
the dispatch_giftwrap_batch invocation and its trailing .await and the
GiftWrapTerminalPolicy::UntrackedFallback argument to locate the exact call
site.

539-557: Mutex .unwrap() may panic on poisoned lock.

If another thread panics while holding active_order_trade_indices, subsequent .unwrap() calls will propagate panics, potentially terminating the listener task. Consider using .lock().expect("context") for clearer panic messages, or handle the poisoned case gracefully if listener continuity is important.

Example with expect
-                    {
-                        let mut indices = active_order_trade_indices.lock().unwrap();
-                        indices.remove(&order_id);
-                    }
+                    {
+                        let mut indices = active_order_trade_indices
+                            .lock()
+                            .expect("active_order_trade_indices mutex poisoned");
+                        indices.remove(&order_id);
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/dm_utils/mod.rs` around lines 539 - 557, The calls to
active_order_trade_indices.lock().unwrap() in the match arms (e.g., inside the
GiftWrapTerminalPolicy::UntrackedFallback branch and the earlier branch) may
panic on a poisoned mutex; replace these .unwrap()s with a safe handling
strategy such as .lock().expect("failed to lock active_order_trade_indices in
listener") or explicitly handle the PoisonError (e.g.,
.lock().unwrap_or_else(|poisoned| poisoned.into_inner())) so the listener task
fails with a clear message or continues gracefully; update both occurrences
where active_order_trade_indices.lock().unwrap() is used to the chosen approach.

296-310: Consider extracting a context struct for the 10 parameters.

The function signature has many parameters. A TradeMessageContext struct holding the shared references (messages, pending_notifications, message_notification_tx, pool) could reduce parameter count and improve readability.

Example context struct
struct DmHandlerContext<'a> {
    messages: &'a Arc<Mutex<Vec<OrderMessage>>>,
    pending_notifications: &'a Arc<Mutex<usize>>,
    message_notification_tx: &'a tokio::sync::mpsc::UnboundedSender<MessageNotification>,
    pool: &'a sqlx::SqlitePool,
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/dm_utils/mod.rs` around lines 296 - 310, The
handle_trade_dm_for_order function has too many parameters; create a context
struct (e.g., TradeMessageContext or DmHandlerContext) that bundles the shared
references messages: Arc<Mutex<Vec<OrderMessage>>>, pending_notifications:
Arc<Mutex<usize>>, message_notification_tx:
tokio::sync::mpsc::UnboundedSender<MessageNotification>, and pool:
sqlx::SqlitePool, then modify handle_trade_dm_for_order to take that single
context struct plus the remaining specific args (order_id: Uuid, trade_index:
i64, message: Message, timestamp: i64, sender: PublicKey, trade_keys: &Keys),
and update all call sites to construct/pass the context instance instead of the
four separate parameters to improve readability and reduce parameter count.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/util/dm_utils/mod.rs`:
- Around line 753-778: The code currently pushes a PendingDmWaiter to
pending_waiters even when client.subscribe(...) fails, causing a silent timeout;
fix by, inside the Err(e) branch after
subscribed_pubkeys.remove(&waiter_pubkey), immediately signal cancellation to
the caller using the waiter’s response_tx (send an error or cancellation result)
and do NOT push the PendingDmWaiter into pending_waiters; in other words, only
push PendingDmWaiter (trade_keys, response_tx) when client.subscribe(...)
returns Ok, and on Err call response_tx to deliver an immediate error before
continuing.

---

Nitpick comments:
In `@src/util/dm_utils/mod.rs`:
- Around line 881-897: The call to dispatch_giftwrap_batch is indented one level
deeper than surrounding code causing inconsistent formatting; adjust the
indentation of the entire call (starting at dispatch_giftwrap_batch(...) through
.await;) to align with the surrounding block indentation so it matches
neighboring statements, referencing the dispatch_giftwrap_batch invocation and
its trailing .await and the GiftWrapTerminalPolicy::UntrackedFallback argument
to locate the exact call site.
- Around line 539-557: The calls to active_order_trade_indices.lock().unwrap()
in the match arms (e.g., inside the GiftWrapTerminalPolicy::UntrackedFallback
branch and the earlier branch) may panic on a poisoned mutex; replace these
.unwrap()s with a safe handling strategy such as .lock().expect("failed to lock
active_order_trade_indices in listener") or explicitly handle the PoisonError
(e.g., .lock().unwrap_or_else(|poisoned| poisoned.into_inner())) so the listener
task fails with a clear message or continues gracefully; update both occurrences
where active_order_trade_indices.lock().unwrap() is used to the chosen approach.
- Around line 296-310: The handle_trade_dm_for_order function has too many
parameters; create a context struct (e.g., TradeMessageContext or
DmHandlerContext) that bundles the shared references messages:
Arc<Mutex<Vec<OrderMessage>>>, pending_notifications: Arc<Mutex<usize>>,
message_notification_tx:
tokio::sync::mpsc::UnboundedSender<MessageNotification>, and pool:
sqlx::SqlitePool, then modify handle_trade_dm_for_order to take that single
context struct plus the remaining specific args (order_id: Uuid, trade_index:
i64, message: Message, timestamp: i64, sender: PublicKey, trade_keys: &Keys),
and update all call sites to construct/pass the context instance instead of the
four separate parameters to improve readability and reduce parameter count.

In `@src/util/order_utils/fetch_scheduler.rs`:
- Around line 228-235: The reconciliation branch currently uses
orders_clone.lock().unwrap() which will panic on a poisoned mutex; match the
pattern used in apply_live_order_update and apply_live_dispute_update by
handling the Result from orders_clone.lock() instead of unwrapping: attempt to
lock, log an error if Err (including context like "orders_reconcile: failed to
lock orders mutex") and return early, otherwise proceed to clear/extend the Vec
and log the count; update both places using unwrap() (e.g., where orders_lock is
acquired) to use this non-panicking, logged handling.
- Line 167: The variable latest_settings is misleading because it's cloned once
at task spawn and never refreshed; rename it to settings_snapshot or
initial_settings to reflect it's a single snapshot taken at spawn time and
update all references (e.g., the cloned binding currently named latest_settings
inside the fetch scheduler task/closure) accordingly so callers/readers
understand currency filter changes won't be applied until restart; ensure
variable rename is applied consistently where latest_settings is used within the
scheduler logic (task spawn/closure) to avoid confusion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e300e333-971e-43de-b172-6a1fb89b47f5

📥 Commits

Reviewing files that changed from the base of the PR and between 927b2a1 and 1efecea.

📒 Files selected for processing (13)
  • docs/STARTUP_AND_CONFIG.md
  • src/main.rs
  • src/ui/key_handler/async_tasks.rs
  • src/ui/key_handler/form_input.rs
  • src/ui/tabs/orders_tab.rs
  • src/util/dm_utils/dm_helpers.rs
  • src/util/dm_utils/mod.rs
  • src/util/order_utils/execute_add_invoice.rs
  • src/util/order_utils/execute_send_msg.rs
  • src/util/order_utils/execute_take_dispute.rs
  • src/util/order_utils/fetch_scheduler.rs
  • src/util/order_utils/send_new_order.rs
  • src/util/order_utils/take_order.rs
✅ Files skipped from review due to trivial changes (1)
  • docs/STARTUP_AND_CONFIG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/ui/key_handler/form_input.rs
  • src/util/order_utils/send_new_order.rs

@arkanoider arkanoider requested review from Catrya and grunch March 23, 2026 22:12
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/util/dm_utils/mod.rs (1)

547-548: Consider logging unsubscribe failures.

The client.unsubscribe() call ignores any errors. While this is acceptable for best-effort cleanup, a debug/trace log on failure would aid troubleshooting relay connection issues.

Suggested enhancement
-                                    client.unsubscribe(subscription_id).await;
+                                    if let Err(e) = client.unsubscribe(subscription_id).await {
+                                        log::debug!(
+                                            "[dm_listener] Failed to unsubscribe {}: {}",
+                                            subscription_id,
+                                            e
+                                        );
+                                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/dm_utils/mod.rs` around lines 547 - 548, The call to
client.unsubscribe(subscription_id).await currently ignores any error; update
the surrounding async block (where client.unsubscribe and subscription_id are
used) to handle the Result by matching or using .await.map_err and log failures
at debug/trace level (e.g., process_logger.debug/trace or the local logger) when
unsubscribe returns Err, while preserving the current best-effort behavior on
success; reference the client.unsubscribe(subscription_id).await invocation and
the subscription_id variable to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/util/dm_utils/mod.rs`:
- Around line 547-548: The call to client.unsubscribe(subscription_id).await
currently ignores any error; update the surrounding async block (where
client.unsubscribe and subscription_id are used) to handle the Result by
matching or using .await.map_err and log failures at debug/trace level (e.g.,
process_logger.debug/trace or the local logger) when unsubscribe returns Err,
while preserving the current best-effort behavior on success; reference the
client.unsubscribe(subscription_id).await invocation and the subscription_id
variable to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0d4c68cf-e3d8-4b6d-a09f-60d1237ab7e5

📥 Commits

Reviewing files that changed from the base of the PR and between 1efecea and e53573e.

📒 Files selected for processing (1)
  • src/util/dm_utils/mod.rs

…improved messages fetch avoiding double nip59 extraction
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@src/util/dm_utils/mod.rs`:
- Around line 851-856: Remove the trailing whitespace on the blank line before
the `if can_decrypt` statement to satisfy CI formatting; locate the block that
computes `ok` using `nip59::extract_rumor(&waiter.trade_keys,
&event).await.is_ok()`, stores it with `rumor_cache.insert(key, ok)`, and then
checks `if can_decrypt`, and simply delete the trailing spaces on the empty line
so the file has no trailing whitespace there.
- Around line 894-899: Remove the trailing whitespace on the blank line between
the block that sets `ok` (which calls `nip59::extract_rumor(&trade_keys,
&event).await.is_ok()` and `rumor_cache.insert(key, ok)`) and the `if
!can_decrypt` check; ensure there are no spaces or tabs on that empty line so CI
no longer flags trailing whitespace (you can locate this around the use of
`can_decrypt`, `key`, `event`, and `trade_keys`).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 014d78bf-7e8d-47c1-87f0-d224b411affc

📥 Commits

Reviewing files that changed from the base of the PR and between e53573e and de6db62.

📒 Files selected for processing (3)
  • .gitignore
  • src/util/dm_utils/dm_helpers.rs
  • src/util/dm_utils/mod.rs
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/util/dm_utils/dm_helpers.rs

Copy link
Member

Choose a reason for hiding this comment

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

These .lock().unwrap() calls (lines 540, 552, 680, 732, 392, 458) would crash the DM router if a mutex gets poisoned. Should these follow the same match Ok/Err + log::warn! pattern used in apply_live_order_update / apply_live_dispute_update in fetch_scheduler.rs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep! Got your point @AndreaDiazCorreia , from a pure logic i agree,but it's quite normal in rust world to just get a lock on mutex with an unwrap. If the lmutex is poisoned you are yet in baaad situation so panic is not a bad thing.

An example.of discussion about this in this example:
https://users.rust-lang.org/t/should-i-unwrap-a-mutex-lock/61519/2

I added the poison check for an AI automatic suggestion, but in the community is totally ok to unwrap for a mutex lock.

)
.await
{
let mut orders_lock = orders_clone.lock().unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

This .lock().unwrap() is in the same file where apply_live_order_update handles the poisoned case gracefully. Should this reconciliation path follow the same pattern for consistency?

Copy link
Collaborator Author

@arkanoider arkanoider Mar 25, 2026

Choose a reason for hiding this comment

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

Yes i will fix the incoherence, but in the opposite direction, I want an exit ( polite... ) from the app if mutex is poisoned. That's a good thing and a well accepted behaviour, in the rare case of poisoning i will prepare a popup message for users telling them that mostrix will close and they need a restart to avoid a bad behaviour.

Copy link
Member

Choose a reason for hiding this comment

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

The comment on line 205 says "Reload currency filters from settings on each fetch", but latest_settings is cloned once at task spawn (line 167). The previous code called load_settings_from_disk() on each tick. Is this intentional, or should the reconciliation branch still reload from disk to pick up runtime filter changes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed, now the comment is clearer. When we launch apply_pending_key_reload settings are read back from file to update them, but the comment could be misleading. Changed var name to reloaded_settings.

          // Read currency filters from the settings snapshot (`reloaded_settings`) each fetch.
          // Note: this does not reload from disk; settings are refreshed when the
          // scheduler tasks are respawned (e.g. via apply_pending_key_reload).
          // An empty list means "no filter" (show all currencies).
          let currencies = reloaded_settings.currencies_filter.clone();

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.

2 participants