Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ When `user_mode = "admin"` and `admin_privkey` is set in `settings.toml`, Mostri
- For each `(dispute, party)` pair, a shared key is derived between the admin key and the party’s trade pubkey and stored as hex in the local DB.
- Admin and party chat via NIP‑59 gift-wrap events addressed to the shared key’s public key, providing restart‑safe, per‑dispute conversations.
- Use **Tab** to switch chat view, **Shift+I** to enable/disable chat input, **PageUp** / **PageDown** to scroll, **End** to jump to latest. Press **Ctrl+S** to save the selected attachment to `~/.mostrix/downloads/`. Press **Shift+F** to open the finalization popup.
- **Finalization**: From the popup you can **Pay Buyer** (`AdminSettle`) or **Refund Seller** (`AdminCancel`), or **Exit**. The execute layer accepts [`BondSlashChoice`](src/util/order_utils/bond_resolution.rs) for optional `bond_resolution` on the wire; the bond-slash picker UI and `bond_enabled` instance gating are still in progress β€” see [docs/FINALIZE_DISPUTES.md](docs/FINALIZE_DISPUTES.md). Finalized disputes (Settled, SellerRefunded, Released) cannot be modified.
- **Finalization**: **Shift+F** opens one popup: **πŸ’° Pay buyer** / **↩️ Refund seller** / **Bond** (inline slash picker overlay; confirm shows bond recap). Wire payload via [`BondSlashChoice`](src/util/order_utils/bond_resolution.rs). **Esc** exits (no Exit button). Instance `bond_enabled` gating still pending β€” see [docs/FINALIZE_DISPUTES.md](docs/FINALIZE_DISPUTES.md). Finalized disputes cannot be settled/canceled again.
- **Settings (admin)**: **Add Dispute Solver** (add another solver by `npub`), **Change Admin Key** (update `admin_privkey`).

For detailed flows and UI, see [docs/ADMIN_DISPUTES.md](docs/ADMIN_DISPUTES.md), [docs/FINALIZE_DISPUTES.md](docs/FINALIZE_DISPUTES.md), and [docs/TUI_INTERFACE.md](docs/TUI_INTERFACE.md).
Expand Down
4 changes: 2 additions & 2 deletions docs/ADMIN_DISPUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ The interface is divided into three main sections:
- **Party switching**: Tab key toggles between buyer and seller
- **Message history**: Per-dispute chat storage with scrolling
- **Dynamic input**: Input box grows from 1 to 10 lines
- **Finalization**: Press **Shift+F** to open the dispute finalization popup from the Disputes in Progress tab (see [FINALIZE_DISPUTES.md](FINALIZE_DISPUTES.md)). Execute path: `execute_finalize_dispute(dispute_id, bond, …)` β†’ `execute_admin_settle` / `execute_admin_cancel` with `bond.to_optional_payload()`. **Planned:** bond-slash picker in the TUI and gating on instance `bond_enabled` (kind 38385); today the UI passes `BondSlashChoice::default()` (`None` β†’ `payload: null`).
- **Finalization**: Press **Shift+F** for the finalize popup (πŸ’° pay buyer / ↩️ refund seller / bond slash with overlay submenu; **Esc** to close). Confirm step shows bond recap. Execute: `execute_finalize_dispute(dispute_id, bond, …)` β†’ `execute_admin_settle` / `execute_admin_cancel` with `bond.to_optional_payload()`. **Pending:** hide bond UI when instance `bond_enabled` is false (kind 38385). See [FINALIZE_DISPUTES.md](FINALIZE_DISPUTES.md).
- **Visual indicators**: Focus states, colors, and icons for clarity

#### Keyboard Navigation
Expand Down Expand Up @@ -353,7 +353,7 @@ pub struct SolverDisputeInfo {

**Identity & Status**:

- **`id`**: Unique identifier (UUID) for the **order** associated with this dispute. Mostrix stores this as the primary key in the `admin_disputes` table and uses it as the ID sent to Mostro when performing admin finalization actions (`AdminSettle` / `AdminCancel`). Optional [`bond_resolution`](https://mostro.network/protocol/admin_settle_order.html) payload is sent via [`BondSlashChoice::to_optional_payload()`](../src/util/order_utils/bond_resolution.rs) on the execute path; the TUI still defaults to no slash until the slash picker lands.
- **`id`**: Unique identifier (UUID) for the **order** associated with this dispute. Mostrix stores this as the primary key in the `admin_disputes` table and uses it as the ID sent to Mostro when performing admin finalization actions (`AdminSettle` / `AdminCancel`). Optional [`bond_resolution`](https://mostro.network/protocol/admin_settle_order.html) payload is sent via [`BondSlashChoice::to_optional_payload()`](../src/util/order_utils/bond_resolution.rs) from the admin’s choice on the finalize popup (default πŸ”“ no slash).
- **`kind`**: Order kind (e.g., "Buy" or "Sell")
- **`status`**: Current dispute status (see [Dispute States](#dispute-states) section)
- **`order_previous_status`**: The order's status before the dispute was initiated
Expand Down
2 changes: 1 addition & 1 deletion docs/CODING_STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ cargo clippy --all-targets --all-features # Lint code
## Dependencies

- **`mostro-core`**: Pin in [`Cargo.toml`](../Cargo.toml) to the same minor line as the Mostro daemon you test against (currently **0.11.3**). Protocol types (`Action`, `Payload`, `BondResolution`, `CantDoReason`, …) must come from `mostro_core::prelude::*` β€” do not duplicate wire shapes in Mostrix.
- **Admin bond slash**: use [`BondSlashChoice`](../src/util/order_utils/bond_resolution.rs) β€” pass through `execute_finalize_dispute(dispute_id, bond, …)` and use `bond.to_optional_payload()` on the wire (`None` β†’ `null`); see [FINALIZE_DISPUTES.md](FINALIZE_DISPUTES.md).
- **Admin bond slash**: use [`BondSlashChoice`](../src/util/order_utils/bond_resolution.rs) β€” TUI labels via `label()` (emoji + text); state on `ReviewingDisputeForFinalization.bond`; pass through `execute_finalize_dispute(dispute_id, bond, …)` and `bond.to_optional_payload()` on the wire (`None` β†’ `null`); see [FINALIZE_DISPUTES.md](FINALIZE_DISPUTES.md).

## Summary Checklist

Expand Down
71 changes: 49 additions & 22 deletions docs/FINALIZE_DISPUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This document describes how admins finalize disputes in Mostrix after reviewing
| **`mostro-core` 0.11.3** | Done | `BondResolution`, `Payload::BondResolution`, `CantDoReason::InvalidPayload` |
| **`BondSlashChoice`** | Done | [`src/util/order_utils/bond_resolution.rs`](../src/util/order_utils/bond_resolution.rs) β€” wire mapping + unit tests |
| **Execute layer** (`execute_admin_settle` / `cancel`) | Done | Accepts `BondSlashChoice`; uses `to_optional_payload()` on the wire |
| **TUI** (slash picker + confirm summary) | Pending | Still two-step: outcome β†’ confirm only |
| **TUI** (slash picker + confirm summary) | Done | Inline bond button + overlay; confirm shows `bond.label()` recap |
| **`bond_enabled` gating** (kind 38385) | Pending | Skip slash UI when instance bonds are off |

Protocol references: [Admin Settle](https://mostro.network/protocol/admin_settle_order.html), [Admin Cancel](https://mostro.network/protocol/admin_cancel_order.html). Daemon bond payout (`Action::AddBondInvoice`, Mostro PR [#738](https://github.com/MostroP2P/mostro/pull/738)) is documented under trade flows, not admin finalization.
Expand All @@ -31,12 +31,12 @@ Protocol references: [Admin Settle](https://mostro.network/protocol/admin_settle
- Visual scrollbar on the right shows position in chat history
5. **Open Finalization**: Press Shift+F to open finalization popup
6. **Review Full Details**: Popup shows complete dispute information
7. **Choose trade outcome**: Use Left/Right arrows β€” **Pay Buyer** (`AdminSettle`) or **Refund Seller** (`AdminCancel`), or **Exit**
8. **Choose bond slash** *(planned)*: Four options β€” no slash, slash buyer, slash seller, slash both (skipped when instance `bond_enabled` is false)
9. **Confirm** *(planned)*: Yes/No popup summarizing outcome + bond choice
10. **Execute**: Press Enter on confirm β€” sends encrypted DM to Mostro
7. **Choose actions on one popup** (Left/Right): **πŸ’° Pay buyer** (`AdminSettle`), **↩️ Refund seller** (`AdminCancel`), or **Bond** (body shows [`BondSlashChoice::label()`](../src/util/order_utils/bond_resolution.rs), default πŸ”“ *No bond slash*). **Esc** closes the popup (no separate Exit button).
8. **Bond slash submenu** (optional): With **Bond** focused, **Enter** opens overlay **βš”οΈ Bond resolution**; ↑/↓ among four labeled choices; **Enter** applies; **Esc** closes submenu only.
9. **Confirm**: **Enter** on pay/refund opens Yes/No β€” title e.g. `⚠️ Confirm πŸ’° Pay buyer`, description + **Bond:** recap (`bond.label()`).
10. **Execute**: **Enter** on Yes β€” sends encrypted DM to Mostro.

**Current UI (until steps 8–9 land):** steps 7 β†’ confirm (no bond slash step); execute uses `BondSlashChoice::default()` (`None` β†’ `payload: null`).
**Current UI:** single finalize popup (7–8) β†’ confirm with bond recap (9) β†’ execute (10). `bond_enabled` gating (hide bond button) still pending.

## Finalization Actions

Expand All @@ -56,19 +56,19 @@ Protocol references: [Admin Settle](https://mostro.network/protocol/admin_settle

### Exit

- **Effect**: Returns to dispute management without taking action
- **Effect**: Press **Esc** on the finalize popup to return to dispute management without taking action
- **Use Case**: Need more information, want to continue chatting with parties

### Bond resolution (anti-abuse bonds)

Independent of settle vs cancel: the admin chooses whether to **slash** posted anti-abuse bonds. Valid on both `admin-settle` and `admin-cancel` only.

| Choice | `slash_seller` | `slash_buyer` | When to use |
|--------|----------------|---------------|-------------|
| No bond slash | false | false | Release bonds; no penalty |
| Slash buyer bond | false | true | Buyer at fault (e.g. false claim on sell order) |
| Slash seller bond | true | false | Seller at fault |
| Slash both bonds | true | true | Both parties violated rules |
| Choice | TUI label (`label()`) | `slash_seller` | `slash_buyer` | When to use |
|--------|----------------------|----------------|---------------|-------------|
| No bond slash | πŸ”“ No bond slash | false | false | Release bonds; no penalty |
| Slash buyer bond | βš”οΈ Slash buyer bond | false | true | Buyer at fault (e.g. false claim on sell order) |
| Slash seller bond | βš”οΈ Slash seller bond | true | false | Seller at fault |
| Slash both bonds | βš”οΈ Slash both bonds | true | true | Both parties violated rules |

Mostrix maps these via [`BondSlashChoice`](../src/util/order_utils/bond_resolution.rs): `to_optional_payload()` sends `payload: null` for **no slash** and `Payload::BondResolution` only when a side is slashed. Use `to_payload()` if you need an explicit `{false, false}` object (same server semantics as null).

Expand Down Expand Up @@ -108,11 +108,15 @@ The popup displays comprehensive dispute information:

> **Note**: The fiat currency code IS displayed alongside the amount. Previous documentation listed "Fiat amount and currency" but did not clarify the format. βœ… **Confirmed working**.

**Action Buttons**:
**Action Buttons** (three columns, Left/Right focus):

- Pay Buyer (Full) - Green background when selected
- Refund Seller (Full) - Red background when selected
- Exit - Gray/default when selected
| Button | When selected | Enter |
|--------|---------------|-------|
| **πŸ’° Pay buyer** | Green highlight | Open confirm (settle) |
| **↩️ Refund seller** | Red highlight | Open confirm (cancel) |
| **Bond** | Primary highlight; body = `bond.label()` | Open bond overlay submenu |

Finalized disputes: pay/refund buttons are dimmed (body `β€”`); use **Esc** to leave. Bond focus may remain on index 2 for display.

### Keyboard Navigation

Expand All @@ -130,9 +134,16 @@ The popup displays comprehensive dispute information:

**In Finalization Popup**:

- Left/Right: Navigate between action buttons (cycles through 3 buttons)
- Enter: Execute selected action
- Esc: Cancel and return to dispute list
- Left/Right: Navigate πŸ’° Pay buyer | ↩️ Refund seller | Bond
- Enter on Pay/Refund: Open confirmation
- Enter on Bond: Open bond submenu overlay
- Esc: Close popup (or close submenu first if open)

**In Bond Slash Submenu (overlay)**:

- Up/Down: Highlight choice (no slash, slash buyer, slash seller, slash both)
- Enter: Apply choice and return to main finalize popup
- Esc: Close submenu without applying

## Protocol Details

Expand Down Expand Up @@ -184,12 +195,26 @@ Internally, Mostrix:

Call chain from the TUI (today):

1. [`execute_finalize_dispute_action`](../src/ui/key_handler/admin_handlers.rs) β€” spawns async task with `bond: BondSlashChoice` (currently `BondSlashChoice::default()` from [`enter_handlers.rs`](../src/ui/key_handler/enter_handlers.rs)).
1. [`execute_finalize_dispute_action`](../src/ui/key_handler/admin_handlers.rs) β€” spawns async task with `bond` from `ConfirmFinalizeDispute` (chosen on the finalize popup / overlay).
2. [`execute_finalize_dispute`](../src/util/order_utils/execute_finalize_dispute.rs) β€” DB guards, then dispatches settle or cancel with the same `bond`.
3. [`execute_admin_settle`](../src/util/order_utils/execute_admin_settle.rs) / [`execute_admin_cancel`](../src/util/order_utils/execute_admin_cancel.rs) β€” `Message::new_dispute(..., bond.to_optional_payload())`, logs include `bond.log_context()`.

Success toasts use the same bond phrase (e.g. `settled (buyer paid) (no bond slash)`).

### UI state (`AdminMode`)

Finalize flow uses a single [`ReviewingDisputeForFinalization`](../src/ui/admin_state.rs) mode (no separate full-screen bond step):

- `dispute_id`, `selected_button_index` (0=pay, 1=refund, 2=bond)
- `bond: BondSlashChoice` (default `None`)
- `slash_submenu_open`, `slash_submenu_index` β€” overlay while picking bond

Confirm: [`ConfirmFinalizeDispute`](../src/ui/admin_state.rs) carries `is_settle`, `bond`, `selected_button` (Yes/No).

### Confirmation popup

Rendered by [`dispute_finalization_confirm.rs`](../src/ui/dispute_finalization_confirm.rs): outcome title with emoji, short description, **Bond:** line with `bond.label()`, Yes/No. **Esc** or **No** returns to finalize popup preserving `bond`.

### Authentication

- Uses admin private key from settings
Expand Down Expand Up @@ -311,7 +336,9 @@ Tab: Switch Party | Shift+F: Finalize | ↑↓: Select Dispute | PgUp/PgDn: Scro
## Related Files

- `src/util/order_utils/bond_resolution.rs` - `BondSlashChoice`, `Payload::BondResolution` mapping, wire tests
- `src/ui/dispute_finalization_popup.rs` - Popup rendering logic
- `src/ui/dispute_finalization_popup.rs` - Finalize popup (πŸ’° pay / ↩️ refund / Bond + overlay trigger)
- `src/ui/dispute_bond_slash_popup.rs` - `render_bond_slash_overlay` on finalize popup
- `src/ui/dispute_finalization_confirm.rs` - Yes/No confirm with bond recap
- `src/util/order_utils/execute_admin_settle.rs` - AdminSettle + `BondSlashChoice` payload
- `src/util/order_utils/execute_admin_cancel.rs` - AdminCancel + `BondSlashChoice` payload
- `src/util/order_utils/execute_finalize_dispute.rs` - DB checks + dispatches settle/cancel
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Index of architecture and feature guides for the Mostrix TUI client. The [root R
## Admin

- **Admin Disputes**: [ADMIN_DISPUTES.md](ADMIN_DISPUTES.md) β€” Tabs, shared-keys chat, workflows
- **Finalize disputes**: [FINALIZE_DISPUTES.md](FINALIZE_DISPUTES.md) β€” Pay buyer / refund seller; **bond slash** via `BondSlashChoice` (execute layer wired; TUI slash picker + `bond_enabled` gating pending)
- **Finalize disputes**: [FINALIZE_DISPUTES.md](FINALIZE_DISPUTES.md) β€” Inline finalize popup (πŸ’° pay / ↩️ refund / bond overlay + confirm recap); `bond_enabled` gating pending

## Contributing & tooling

Expand Down
10 changes: 9 additions & 1 deletion src/ui/admin_state.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::shared::permissions::SolverPermission;
use crate::ui::KeyInputState;
use crate::util::order_utils::BondSlashChoice;

#[derive(Clone, Debug)]
pub struct AddSolverState {
Expand All @@ -24,13 +25,20 @@ pub enum AdminMode {
ManagingDispute, // Mode for "Disputes in Progress" tab
ReviewingDisputeForFinalization {
dispute_id: uuid::Uuid,
/// Index of the selected button: 0=Pay Buyer, 1=Refund Seller, 2=Exit
/// Index of the selected button: 0=Pay Buyer, 1=Refund Seller, 2=Bond slash
selected_button_index: usize,
/// Anti-abuse bond slash choice (default: no slash).
bond: BondSlashChoice,
/// When true, show the bond slash overlay submenu.
slash_submenu_open: bool,
/// Highlighted index in [`BondSlashChoice::ALL`] while submenu is open.
slash_submenu_index: usize,
},
ConfirmFinalizeDispute {
dispute_id: uuid::Uuid,
/// true=Pay Buyer, false=Refund Seller
is_settle: bool,
bond: BondSlashChoice,
/// true=Yes, false=No
selected_button: bool,
},
Expand Down
95 changes: 95 additions & 0 deletions src/ui/dispute_bond_slash_popup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};

use super::{helpers, BACKGROUND_COLOR, PRIMARY_COLOR};
use crate::util::order_utils::BondSlashChoice;

/// Centered bond-slash submenu overlay on top of the finalize popup.
pub fn render_bond_slash_overlay(
f: &mut ratatui::Frame,
parent_area: Rect,
selected_choice_index: usize,
) {
let popup_width = 52.min(parent_area.width.saturating_sub(4));
let popup_height = 12.min(parent_area.height.saturating_sub(2));
let popup = helpers::create_centered_popup(parent_area, popup_width, popup_height);
f.render_widget(Clear, popup);

let block = Block::default()
.title("βš”οΈ Bond resolution")
.borders(Borders::ALL)
.style(Style::default().bg(BACKGROUND_COLOR).fg(PRIMARY_COLOR));

let inner = block.inner(popup);
f.render_widget(block, popup);

let chunks = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(4),
Constraint::Length(1),
],
)
.split(inner);

f.render_widget(
Paragraph::new("Choose how to resolve anti-abuse bonds:")
.alignment(ratatui::layout::Alignment::Center),
chunks[0],
);

let mut choice_lines: Vec<Line> = Vec::with_capacity(BondSlashChoice::ALL.len());
for (i, choice) in BondSlashChoice::ALL.iter().enumerate() {
let selected = i == selected_choice_index;
let style = if selected {
Style::default()
.bg(PRIMARY_COLOR)
.fg(BACKGROUND_COLOR)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
let prefix = if selected { "β–Ά " } else { " " };
choice_lines.push(Line::from(vec![Span::styled(
format!("{}{}", prefix, choice.label()),
style,
)]));
}
f.render_widget(
Paragraph::new(choice_lines).alignment(ratatui::layout::Alignment::Center),
chunks[3],
);

f.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(
"↑↓",
Style::default()
.fg(PRIMARY_COLOR)
.add_modifier(Modifier::BOLD),
),
Span::raw(" select "),
Span::styled(
"Enter",
Style::default()
.fg(PRIMARY_COLOR)
.add_modifier(Modifier::BOLD),
),
Span::raw(" apply "),
Span::styled(
"Esc",
Style::default()
.fg(PRIMARY_COLOR)
.add_modifier(Modifier::BOLD),
),
Span::raw(" back"),
]))
.alignment(ratatui::layout::Alignment::Center),
chunks[4],
);
}
Loading
Loading