diff --git a/docs/CODING_STANDARDS.md b/docs/CODING_STANDARDS.md index 150e505..931db2d 100644 --- a/docs/CODING_STANDARDS.md +++ b/docs/CODING_STANDARDS.md @@ -73,7 +73,7 @@ fn foo(msg: &crate::ui::DisputeChatMessage) { /* ... */ } - **Extract helpers**: Break complex functions into smaller, focused helpers. - **Use private functions**: Create internal helper functions when needed. -**Example**: If `handle_key_event` grows too large, split it into `handle_navigation_keys`, `handle_form_input`, etc. +**Example**: If `handle_key_event` grows too large, split it into `handle_navigation_keys`, `handle_form_input`, etc. Form typing lives in **`src/ui/key_handler/form_input.rs`**; global keys like **`n`** (cancel) and **`c`** (copy) must not swallow characters when **`is_creating_order_text_input`** is true — add guarded arms in `mod.rs` before the generic `Char(_)` handler. ### 5. Module and Function Organization diff --git a/docs/DATABASE.md b/docs/DATABASE.md index d7a84da..ef89556 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -238,6 +238,7 @@ The `orders` table is essential for: - **Trade Keys**: Stored as hex-encoded secret keys. **Critical security data** - these keys are needed to decrypt messages for each trade. - **Order Updates**: Orders are updated (not just inserted) when status changes, using upsert logic. Status writes are guarded for monotonic progression where applicable (stale/out-of-order DMs should not move an order backward in the trade phase graph); see `should_apply_status_transition` in `src/util/order_utils/helper.rs` and **DM_LISTENER_FLOW.md**. +- **Relay terminal reconcile**: `src/util/order_utils/relay_order_db_reconcile.rs` can set **`orders.status`** from the latest Mostro nostr order event when the relay reports a **terminal** status and the local row is still non-terminal. Runs on startup and on the periodic orders updater (`fetch_scheduler.rs`). Targeted mode (`Order::list_ids_for_targeted_relay_reconcile`) only considers rows with **`trade_keys`** set and status not in `TERMINAL_ORDER_HISTORY_STATUSES` (`helper.rs`). Does not replace trade-DM hydration for active phases; complements it when the client missed the final DM. - **Maker/Taker persistence**: `save_order(..., is_maker)` sets `is_mine` from runtime flow (`true` for new-order flow, `false` for take-order flow). **Source**: `src/models.rs:154` diff --git a/docs/DM_LISTENER_FLOW.md b/docs/DM_LISTENER_FLOW.md index 4214d63..d0c8654 100644 --- a/docs/DM_LISTENER_FLOW.md +++ b/docs/DM_LISTENER_FLOW.md @@ -159,6 +159,9 @@ This function is where `OrderMessage` is created/updated and pushed into `messag Key behaviors: +- **Early return for non-trade hydration actions** + `Action::NewOrder` and **`Action::CantDo`** return immediately (same as book-side noise). **`CantDo`** is an error response from Mostro (`Payload::CantDo`); it is handled on the **waiter** path (`order_utils/helper.rs` → user-facing `OperationResult`) and must **not** upsert SQLite or replace the per-order Messages row. Treating `CantDo` like a normal trade DM caused bogus order hydration after failed takes or invalid actions. + - **DB refresh/upsert for certain actions** For `add-invoice`, `pay-invoice`, and **`pay-bond-invoice`** (Mostro Phase 1.5+ anti-abuse bond) where the payload embeds an order, the listener persists/upserts the order row (including request id when available). `pay-bond-invoice` is additionally allow-listed for null `request_id` DMs (`src/util/order_utils/helper.rs`) since Mostro emits these unsolicited after a take. diff --git a/docs/MESSAGE_FLOW_AND_PROTOCOL.md b/docs/MESSAGE_FLOW_AND_PROTOCOL.md index 256715d..1eb5383 100644 --- a/docs/MESSAGE_FLOW_AND_PROTOCOL.md +++ b/docs/MESSAGE_FLOW_AND_PROTOCOL.md @@ -488,12 +488,34 @@ After a successful trade, Mostro may prompt with a DM whose **`action`** is **`r - **UI**: **`UiMode::RatingOrder`** (`src/ui/app_state.rs`) — star row **1–5** (`mostro_core::MIN_RATING` / `MAX_RATING`), **Left/Right** or **+/-** to adjust, **Enter** to submit, **Esc** to dismiss. Rendered in **`src/ui/tabs/tab_content.rs`** (`render_rating_order`), opened from Messages **Enter** when the selected message’s action is **`Rate`** (`src/ui/key_handler/enter_handlers.rs`). - **Send path**: **`execute_rate_user`** in **`src/util/order_utils/execute_send_msg.rs`** builds **`Message::new_order`** with **`Action::RateUser`**, **`Payload::RatingUser(rating)`**, and the trade **`order_id`**; **identity + trade keys** and **`send_dm` / `wait_for_dm`** match other trade messages. The response is expected to be **`Action::RateReceived`**. No counterparty pubkey is sent — Mostro resolves the peer server-side. +## Relay → SQLite order status reconcile + +Mostrix can align local **`orders.status`** with **terminal** states published on Mostro nostr order events when trade DMs were missed or the client was offline. + +**Source**: `src/util/order_utils/relay_order_db_reconcile.rs`, wired from `src/util/order_utils/fetch_scheduler.rs` and startup in `src/main.rs`. + +| Path | When | What | +|------|------|------| +| **Bulk** | Orders updater tick (~30s) + **startup** | `fetch_mostro_order_events` → `aggregate_latest_orders_by_id` → `reconcile_terminal_order_statuses_from_relay` | +| **Targeted** | Same tick + **startup** | `Order::list_ids_for_targeted_relay_reconcile` (non-terminal rows with `trade_keys`) → round-robin up to **`TARGETED_RELAY_RECONCILE_MAX_PER_TICK`** (5) per-order fetches → `reconcile_one_order_if_terminal` | + +`reconcile_one_order_if_terminal` only writes when the relay snapshot status is **terminal** (`is_terminal_trade_status`) and passes **`should_apply_status_transition`** (same monotonic rules as DM updates). Pending orders on the book are not “healed” from relay unless the relay reports a terminal outcome (e.g. **Expired**). + ## Messages tab: trade timeline stepper (buy and sell listings) -The Messages detail panel shows a **six-step** timeline for trades with known **`order_kind`**. The highlighted column comes from **`message_trade_timeline_step`** → **`FlowStep`** (`src/ui/orders.rs`): **`BuyFlowStep(StepLabelsBuy)`** or **`SellFlowStep(StepLabelsSell)`**, each with discriminants **1…6** for UI columns (sell swaps the first two phase columns vs buy). Resolution dispatches to **`buy_listing_flow_step`** or **`sell_listing_flow_step`**, combining **`OrderMessage::order_status`**, **`is_mine`** (maker/taker), and **`action`**, via **`listing_step_from_status(order_kind, status)`** (kind-specific status mapping) and kind-specific **`_flow_step_from_action`**. **`Action::Rate`** / **`RateReceived`** are handled before status so **`rate`** DMs without a full order payload still highlight the final step. +The Messages detail panel shows a **six-step** timeline for trades with known **`order_kind`**. The highlighted column comes from **`message_trade_timeline_step`** → **`FlowStep`** (`src/ui/orders.rs`): **`BuyFlowStep(StepLabelsBuy)`** or **`SellFlowStep(StepLabelsSell)`**. Step enums use **`repr(u8)`** discriminants passed to **`FlowStep::step_number()`** (UI columns are **1…6** in `message_flow_tab.rs`; discriminant **0** = no highlight). + +Resolution dispatches to **`buy_listing_flow_step`** or **`sell_listing_flow_step`**, combining **`OrderMessage::order_status`**, **`is_mine`** (maker/taker), and **`action`**, via **`listing_step_from_status(order_kind, status)`** (kind-specific status mapping) and kind-specific **`_flow_step_from_action`**. **`Action::Rate`** / **`RateReceived`** are handled before status so **`rate`** DMs without a full order payload still highlight the final step. + +- **`Status::Pending`** / **`Status::WaitingTakerBond`** → **`StepPendingOrder`** (discriminant **0**): stepper shows **no** green/current column (all gray) until payment/bond phases start. +- **`Status::Success`** → final column (**`StepRate`**, discriminant **6**); avoids snapping back to an older step when reboot replay delivers a pre-success DM after the trade completed. Step **wording** (strings per column) lives in **`src/ui/constants.rs`** (`StepLabel`, buy/sell step arrays); **`listing_timeline_labels`** selects the array by kind and role. +**Sidebar / info popups**: `message_action_compact_label_for_message` maps status to short labels (**Pending order**, **Trade Completed**, …) so list text stays accurate after hydration. + +**Success-before-DM placeholder**: `try_placeholder_order_message_from_success` builds one synthetic **`OrderMessage`** when `OperationResult::Success` lands before any DM row (My Trades sidebar); placeholder **`action`** is status-driven and never uses synthetic **`take-buy`** / **`take-sell`** (those break Messages **Enter**). Applied in `order_ch_mng.rs` via `insert_placeholder_order_message_if_needed`. + See **[buy order flow.md](buy%20order%20flow.md)** and **[sell order flow.md](sell%20order%20flow.md)** for product context and **[TUI_INTERFACE.md](TUI_INTERFACE.md)** for **`UiMode`** overlays. ## Error Handling Patterns diff --git a/docs/README.md b/docs/README.md index 0c6c74a..67f6e11 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,15 +5,15 @@ Index of architecture and feature guides for the Mostrix TUI client. The [root R ## Core runtime & data - **Startup & Configuration**: [STARTUP_AND_CONFIG.md](STARTUP_AND_CONFIG.md) — Boot sequence, settings, background tasks, DM router wiring, reconnect -- **DM listener & router**: [DM_LISTENER_FLOW.md](DM_LISTENER_FLOW.md) — `listen_for_order_messages`, TrackOrder vs waiter, startup `fetch_events` replay, in-memory `OrderMessage` list +- **DM listener & router**: [DM_LISTENER_FLOW.md](DM_LISTENER_FLOW.md) — `listen_for_order_messages`, TrackOrder vs waiter, startup `fetch_events` replay, in-memory `OrderMessage` list; **`Action::CantDo`** ignored in `handle_trade_dm_for_order` (errors use waiter / `OperationResult`, not Messages upserts) - **Message Flow & Protocol**: [MESSAGE_FLOW_AND_PROTOCOL.md](MESSAGE_FLOW_AND_PROTOCOL.md) — How Mostrix talks to Mostro over Nostr (orders, GiftWrap, restarts, cooperative cancel / `TradeClosed`) - **PoW & outbound events**: [POW_AND_OUTBOUND_EVENTS.md](POW_AND_OUTBOUND_EVENTS.md) — Instance `pow` (kind 38385), `nostr_pow_from_instance`, Gift Wrap outer mining (`gift_wrap_from_seal_with_pow`) -- **Database**: [DATABASE.md](DATABASE.md) — SQLite schema, `orders` / `users` / `admin_disputes`, migrations +- **Database**: [DATABASE.md](DATABASE.md) — SQLite schema, `orders` / `users` / `admin_disputes`, migrations; **relay → SQLite reconcile** for terminal order statuses (`relay_order_db_reconcile.rs`) - **Key Management**: [KEY_MANAGEMENT.md](KEY_MANAGEMENT.md) — Deterministic derivation (NIP-06 path), identity vs trade keys ## UI & order flows -- **TUI Interface**: [TUI_INTERFACE.md](TUI_INTERFACE.md) — Navigation, modes, state; My Trades (static `order_chat_static` header vs `build_active_order_chat_list` live fields) +- **TUI Interface**: [TUI_INTERFACE.md](TUI_INTERFACE.md) — Navigation, modes, state; create-order form input; My Trades (static `order_chat_static` header vs `build_active_order_chat_list` live fields); Messages timeline (`StepPendingOrder` = no highlighted column while `Pending` / `WaitingTakerBond`) - **UI constants** (`src/ui/constants.rs`): Shared copy (footers, help, **`StepLabel`** for the Messages tab buy/sell timeline) - **Buy order flow (spec)**: [buy order flow.md](buy%20order%20flow.md) — Phase 1.5+ optional **anti-abuse bond** (`PayBondInvoice` / `WaitingTakerBond`) included as phase 0 for the taker - **Sell order flow (spec)**: [sell order flow.md](sell%20order%20flow.md) — Phase 1.5+ optional **anti-abuse bond** for the taker (buyer) diff --git a/docs/STARTUP_AND_CONFIG.md b/docs/STARTUP_AND_CONFIG.md index 8b8130f..f1decbb 100644 --- a/docs/STARTUP_AND_CONFIG.md +++ b/docs/STARTUP_AND_CONFIG.md @@ -135,14 +135,15 @@ Current startup behavior: Several background tasks are spawned to keep the UI and data in sync: 1. **Order Refresh**: Periodically fetches pending orders from Mostro. -2. **Trade Message Listener**: Listens for new messages related to active orders. -3. **Network Status Monitor**: +2. **Relay order DB reconcile** (startup + ~30s orders updater): `run_relay_order_db_reconcile_once` (bulk terminal sync from nostr order events) and `run_targeted_relay_order_db_reconcile_tick` (round-robin per-order fetch for local non-terminal trades with keys). See `relay_order_db_reconcile.rs` and **MESSAGE_FLOW_AND_PROTOCOL.md** (Relay → SQLite section). +3. **Trade Message Listener**: Listens for new messages related to active orders. +4. **Network Status Monitor**: - `spawn_network_status_monitor` runs every 5 seconds. - Re-checks relay reachability from disk settings and emits `NetworkStatus::Offline/Online`. - On `Offline`, startup overlay text indicates automatic retry. - On `Online`, `main.rs` triggers `reload_runtime_session_after_reconnect(...)` to reconnect and reload runtime background tasks. -4. **Admin Chat Scheduler** (shared-key model): +5. **Admin Chat Scheduler** (shared-key model): - In the main event loop, when `user_role == Admin`, a 5-second interval triggers `spawn_admin_chat_fetch` (see `src/util/order_utils/fetch_scheduler.rs`). - A **single-flight guard** (`CHAT_MESSAGES_SEMAPHORE`: `AtomicBool`) ensures only one admin chat fetch runs at a time; overlapping ticks skip spawning a new fetch until the current one completes. - For each in-progress dispute, rebuilds per-party shared `Keys` from `buyer_shared_key_hex` / `seller_shared_key_hex` stored in the `admin_disputes` table. @@ -154,7 +155,7 @@ Several background tasks are spawned to keep the UI and data in sync: **Source**: `src/main.rs` (background task setup), `src/util/order_utils/fetch_scheduler.rs` (admin chat scheduler), `src/ui/helpers/startup.rs` (`apply_admin_chat_updates`) -5. **DM Router Wiring (trade messages)**: +6. **DM Router Wiring (trade messages)**: - App channel creation includes `dm_subscription_tx` / `dm_subscription_rx`. - `set_dm_router_cmd_tx(dm_subscription_tx.clone())` publishes the sender globally for `wait_for_dm` (returns `Result`; startup fails fast if the mutex is poisoned). - Before spawning the listener, `hydrate_startup_active_order_dm_state` loads non-terminal orders from SQLite and returns `active_order_trade_indices` plus `order_last_seen_dm_ts` cursors; `main.rs` seeds the shared active-order map. diff --git a/docs/TUI_INTERFACE.md b/docs/TUI_INTERFACE.md index b72ecc6..7d4a9c4 100644 --- a/docs/TUI_INTERFACE.md +++ b/docs/TUI_INTERFACE.md @@ -199,7 +199,9 @@ The `handle_key_event` function dispatches keys based on the current `UiMode`. ### Specialized Input -- **Forms**: Character input and Backspace are handled by `handle_char_input` and `handle_backspace` for fields in `FormState`. +- **Forms**: Character input and Backspace are handled by `form_input::handle_char_input` and `form_input::handle_backspace` for fields in `FormState` while `UiMode::UserMode(UserMode::CreatingOrder(_))`. + - **Create New Order** (`src/ui/order_form.rs`, `src/ui/orders.rs` — `FormState` / `FormField`): **Tab** / **Shift+Tab** cycle focus; **Space** toggles buy/sell on **Order Type** and single/range on **Fiat Amount**; **Enter** submits; **Esc** cancels. + - **Global shortcut guard**: `n` / `N` (cancel) and `c` / `C` (copy invoice / observer clear) are handled before the generic `Char(_)` arm in `key_handler/mod.rs`. When a **text** field is focused (`is_creating_order_text_input` in `form_input.rs` — any field except **Order Type**), those keys are routed to form typing instead (fixes payment method labels like **SEPA** / **Bizum**). Outside the form, `n` still drives confirmation cancel (`handle_cancel_key`); `c` still copies PayInvoice / PayBondInvoice invoices. - **Invoices**: `handle_invoice_input` handles text entry for Lightning invoices, including support for bracketed paste mode. - **Paste support**: The event loop now centralizes paste routing for active inputs and supports: - `Event::Paste(...)` (bracketed paste) @@ -228,6 +230,10 @@ Renders a table of pending orders from the Mostro network. Status and order kind Displays a list of direct messages related to the user's trades. Messages are tracked as `read` or `unread`. The detail panel includes a **trade timeline stepper** (six columns): **`FlowStep`** from `src/ui/orders.rs` (`message_trade_timeline_step`), with per-column copy from **`src/ui/constants.rs`** (`listing_timeline_labels`). See [buy order flow.md](buy%20order%20flow.md) and [sell order flow.md](sell%20order%20flow.md). +- **Sidebar labels**: `message_action_compact_label_for_message` prefers **`order_status`** over raw **`action`** (e.g. **Pending order**, **Trade Completed**) so reboot replay does not show stale action text. Non-actionable rows opened with **Enter** use that compact label in an **`OperationResult::Info`** popup (`enter_handlers.rs`). +- **Pending / bond-pending timeline**: `Status::Pending` and `Status::WaitingTakerBond` map to **`StepPendingOrder`** (discriminant **0**). The stepper uses `step_number() == 0`, so **no column is highlighted** (all steps gray) until the trade advances — avoids falsely lighting step 1 while the order is still on the book or awaiting bond. +- **Post-success placeholder row**: when **`OperationResult::Success`** arrives before any DM row exists, `try_placeholder_order_message_from_success` (`orders.rs`) inserts one synthetic **`OrderMessage`** for My Trades / Messages (action from status: maker → `NewOrder`, taker → `PayBondInvoice` / `WaitingSellerToPay` / etc.; never synthetic `take-buy` / `take-sell`). **`main.rs`** also resyncs My Trades from DB after **`Success`**, not only after history delete. + - **Enter** on a row: opens an invoice popup, a confirmation popup, the **rating** overlay (`RatingOrder`) when the daemon sent **`action: rate`**, or an info line for other actions (`src/ui/key_handler/enter_handlers.rs`). - **Rating overlay**: `render_rating_order` in `src/ui/tabs/tab_content.rs`; keys **Left/Right** or **+/-** adjust stars, **Enter** submits, **Esc** closes. - **Invoice popups (`NewMessageNotification`)**: @@ -249,7 +255,9 @@ Displays a list of direct messages related to the user's trades. Messages are tr A stateful form for creating new orders. It supports both fixed amounts and fiat ranges. -**Source**: `src/ui/order_form.rs` +**Fields** (`FormField` in `src/ui/orders.rs`): Order Type (toggle), Currency, Amount (sats), Fiat Amount (+ optional max when range), Payment Method, Premium (%), Invoice (optional), Expiration (days). + +**Source**: `src/ui/order_form.rs`, `src/ui/key_handler/form_input.rs`, `src/ui/key_handler/navigation.rs` (Tab focus) ### My Trades (Order In Progress) updates diff --git a/docs/buy order flow.md b/docs/buy order flow.md index fd61bf4..f64a136 100644 --- a/docs/buy order flow.md +++ b/docs/buy order flow.md @@ -106,8 +106,8 @@ In **`src/ui/key_handler/enter_handlers.rs`**, Messages **Enter** is routed by * ## Implementation notes (non-normative) -- **Trade timeline step** (`message_trade_timeline_step` in `src/ui/orders.rs`): returns **`FlowStep`** — either **`BuyFlowStep(StepLabelsBuy)`** or **`SellFlowStep(StepLabelsSell)`**. Each inner enum uses **discriminants 1…6** for the stepper column; **sell** uses a **different order** for the first two phases than buy (`StepLabelsSell`: `StepBuyerInvoice` = column 1, `StepSellerPayment` = column 2; see `src/ui/orders.rs`). -- **Pipeline:** **`buy_listing_flow_step`** / **`sell_listing_flow_step`** → early **`Action::Rate`** / **`RateReceived`** → **`listing_step_from_status(order_kind, status)`** (same Mostro statuses, **kind-specific** mapping to columns) → **`buy_listing_flow_step_from_action`** / **`sell_listing_flow_step_from_action`**. **`Status::Success`** does **not** pick step 6 alone; **`Action::Rate`** / **`RateReceived`** run before status so **`rate`** + **`success`** still highlights rate. +- **Trade timeline step** (`message_trade_timeline_step` in `src/ui/orders.rs`): returns **`FlowStep`** — either **`BuyFlowStep(StepLabelsBuy)`** or **`SellFlowStep(StepLabelsSell)`**. Inner enums use **`repr(u8)`** discriminants: **`StepPendingOrder = 0`** (no highlighted column — all steps gray) for **`Status::Pending`** / **`WaitingTakerBond`**; phases **1…6** for active trade steps. **Sell** swaps the first two payment columns vs buy (`StepLabelsSell`: `StepBuyerInvoice` = 1, `StepSellerPayment` = 2; see `src/ui/orders.rs`). +- **Pipeline:** **`buy_listing_flow_step`** / **`sell_listing_flow_step`** → early **`Action::FiatSentOk`** → **`listing_step_from_status(order_kind, status)`** when **`order_status`** is set → **`buy_listing_flow_step_from_action`** / **`sell_listing_flow_step_from_action`**. **`Status::Success`** maps to **`StepRate`** (column **6**) via **`listing_step_from_status`**, keeping completed trades on the final column after reboot replay. **`Action::Rate`** / **`RateReceived`** are resolved in the action fallbacks (only when status is missing or unmapped); both paths highlight **`StepRate`** (6), so a **`rate`** DM without hydrated status and a row with **`Status::Success`** present the same final step. - **Text labels** (top/bottom lines per column): **`src/ui/constants.rs`** — **`BUY_ORDER_FLOW_STEPS_*`**, **`SELL_ORDER_FLOW_STEPS_*`**, **`GENERIC_ORDER_FLOW_STEPS_TAKER`**; **`listing_timeline_labels`** in `orders.rs` picks the array by **`order_kind`** and **`is_mine`**. Rendering: **`src/ui/tabs/message_flow_tab.rs`**. - **Follow-up:** stricter **Enter** / popups (status + role + **`kind`**); see [MESSAGE_FLOW_AND_PROTOCOL.md](MESSAGE_FLOW_AND_PROTOCOL.md#messages-tab-trade-timeline-stepper-buy-and-sell-listings). Sell detail: [sell order flow.md](sell%20order%20flow.md). diff --git a/docs/sell order flow.md b/docs/sell order flow.md index 418a6fd..c3f2a89 100644 --- a/docs/sell order flow.md +++ b/docs/sell order flow.md @@ -44,7 +44,7 @@ Phases (labels from `SELL_ORDER_FLOW_STEPS_TAKER`): ## Implementation notes (non-normative) -- **Timeline step resolution** (`src/ui/orders.rs`): **`message_trade_timeline_step`** dispatches on **`order_kind`**. For **`Kind::Sell`**, **`sell_listing_flow_step`** returns **`FlowStep::SellFlowStep(StepLabelsSell)`** with the same pipeline as buy: early **`Action::Rate`** / **`RateReceived`**, then **`listing_step_from_status(Kind::Sell, status)`**, then **`sell_listing_flow_step_from_action`** (maker = seller, taker = buyer). **`Status::Success`** still does not pick step 6 by itself; rate is action-driven. **`Status::WaitingTakerBond`** maps to `StepBuyerInvoice` for sell listings (and `StepSellerPayment` for buy listings) — same pre-trade column as `Pending` / `WaitingPayment` — so the timeline does not jump while the taker is reviewing the bond popup. +- **Timeline step resolution** (`src/ui/orders.rs`): **`message_trade_timeline_step`** dispatches on **`order_kind`**. For **`Kind::Sell`**, **`sell_listing_flow_step`** returns **`FlowStep::SellFlowStep(StepLabelsSell)`** with the same pipeline as buy: early **`Action::Rate`** / **`RateReceived`**, then **`listing_step_from_status(Kind::Sell, status)`**, then **`sell_listing_flow_step_from_action`** (maker = seller, taker = buyer). **`Status::Success`** maps to **`StepRate`** (column 6) via status so completed trades stay on the final column after reboot replay. **`Status::Pending`** and **`Status::WaitingTakerBond`** map to **`StepPendingOrder`** (discriminant **0**): the stepper highlights **no** column until bond/payment phases begin (avoids lighting step 1 while the order is still pending or the bond popup is open). - **Labels**: **`listing_timeline_labels`** chooses **`SELL_ORDER_FLOW_STEPS_MAKER`** / **`SELL_ORDER_FLOW_STEPS_TAKER`** from **`src/ui/constants.rs`** when **`order_kind == Sell`**; column **indices** come from **`StepLabelsSell`** (see `orders.rs`). - **Tests**: `timeline_step_tests` in `src/ui/orders.rs` cover representative sell maker/taker and status cases. diff --git a/src/ui/key_handler/form_input.rs b/src/ui/key_handler/form_input.rs index 1c36e95..c1a0c86 100644 --- a/src/ui/key_handler/form_input.rs +++ b/src/ui/key_handler/form_input.rs @@ -2,6 +2,15 @@ use crate::ui::orders::FormField; use crate::ui::{AppState, TakeOrderState, UiMode, UserMode}; use crossterm::event::KeyCode; +/// True when the create-order form has a text-editable field focused (not buy/sell toggle). +pub fn is_creating_order_text_input(app: &AppState) -> bool { + matches!( + app.mode, + UiMode::UserMode(UserMode::CreatingOrder(ref form)) + if form.focused != FormField::OrderType + ) +} + /// Handle character input for forms pub fn handle_char_input( code: KeyCode, @@ -98,3 +107,23 @@ pub fn handle_backspace(app: &mut AppState, validate_range_amount: &dyn Fn(&mut } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::{FormState, UserRole}; + + #[test] + fn creating_order_text_input_excludes_order_type_toggle() { + let mut app = AppState::new(UserRole::User); + let mut form = FormState::new_default_form(); + form.focused = FormField::PaymentMethod; + app.mode = UiMode::UserMode(UserMode::CreatingOrder(form)); + assert!(is_creating_order_text_input(&app)); + + if let UiMode::UserMode(UserMode::CreatingOrder(ref mut form)) = app.mode { + form.focused = FormField::OrderType; + } + assert!(!is_creating_order_text_input(&app)); + } +} diff --git a/src/ui/key_handler/mod.rs b/src/ui/key_handler/mod.rs index 9ff27d3..e8f7427 100644 --- a/src/ui/key_handler/mod.rs +++ b/src/ui/key_handler/mod.rs @@ -76,7 +76,7 @@ pub use async_tasks::{ pub use confirmation::{handle_cancel_key, handle_confirm_key}; pub use enter_handlers::handle_enter_key; pub use esc_handlers::handle_esc_key; -pub use form_input::{handle_backspace, handle_char_input}; +pub use form_input::{handle_backspace, handle_char_input, is_creating_order_text_input}; pub use input_helpers::{handle_invoice_input, handle_key_input}; pub use navigation::{handle_navigation, handle_tab_navigation}; pub use settings::handle_mode_switch; @@ -1111,10 +1111,18 @@ pub fn handle_key_event( } // 'q' key removed - use Exit tab instead. // For confirmations, prefer using Enter on the focused button instead of 'y'/'n'. + KeyCode::Char('n') | KeyCode::Char('N') if is_creating_order_text_input(app) => { + handle_char_input(code, app, validate_range_amount); + Some(true) + } KeyCode::Char('n') | KeyCode::Char('N') => { handle_cancel_key(app); Some(true) } + KeyCode::Char('c') | KeyCode::Char('C') if is_creating_order_text_input(app) => { + handle_char_input(code, app, validate_range_amount); + Some(true) + } KeyCode::Char('c') | KeyCode::Char('C') => { // In Observer tab, Ctrl+C clears inputs and decrypted content if let (Tab::Admin(AdminTab::Observer), true) = (