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**. Anti-abuse **bond slash** options (none / buyer / seller / both) per [Mostro protocol](https://mostro.network/protocol/admin_settle_order.html) are in progress — see [docs/FINALIZE_DISPUTES.md](docs/FINALIZE_DISPUTES.md). Finalized disputes (Settled, SellerRefunded, Released) cannot be modified.
- **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.
- **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)). **Planned:** after choosing Pay Buyer or Refund Seller, pick a **bond slash** option (none / buyer / seller / both) when the Mostro instance has anti-abuse bonds enabled; [`BondSlashChoice`](../src/util/order_utils/bond_resolution.rs) and protocol `bond_resolution` payload are implemented, UI and `execute_admin_*` wiring are not yet.
- **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`).
- **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 (slash buyer/seller/both/none) will be attached via [`BondSlashChoice`](../src/util/order_utils/bond_resolution.rs) once the execute layer and TUI are wired.
- **`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.
- **`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) for `admin-settle` / `admin-cancel` payloads; see [FINALIZE_DISPUTES.md](FINALIZE_DISPUTES.md).
- **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).

## Summary Checklist

Expand Down
21 changes: 16 additions & 5 deletions docs/FINALIZE_DISPUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ 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`) | Pending | Still sends `payload: null`; step 3 will pass `BondSlashChoice` |
| **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 |
| **`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 @@ -35,7 +36,7 @@ Protocol references: [Admin Settle](https://mostro.network/protocol/admin_settle
9. **Confirm** *(planned)*: Yes/No popup summarizing outcome + bond choice
10. **Execute**: Press Enter on confirm — sends encrypted DM to Mostro

**Current UI (until step 4–5 land):** steps 7 → confirm (no bond slash step); wire payload is always `null`.
**Current UI (until steps 8–9 land):** steps 7 → confirm (no bond slash step); execute uses `BondSlashChoice::default()` (`None` → `payload: null`).

## Finalization Actions

Expand Down Expand Up @@ -149,7 +150,7 @@ Message::new_dispute(
None,
None,
Action::AdminSettle, // or AdminCancel
bond.to_optional_payload(), // None for no slash; Some(BondResolution) when slashing; today: execute still passes None
bond.to_optional_payload(), // None → null; slash variants → BondResolution
)
```

Expand Down Expand Up @@ -179,6 +180,16 @@ Internally, Mostrix:
- Reads the corresponding **order ID** from the `id` column.
- Uses that order ID as the first parameter of `Message::new_dispute`, matching what Mostro expects for finalization actions.

### Execute API

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)).
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)`).

### Authentication

- Uses admin private key from settings
Expand Down Expand Up @@ -301,8 +312,8 @@ Tab: Switch Party | Shift+F: Finalize | ↑↓: Select Dispute | PgUp/PgDn: Scro

- `src/util/order_utils/bond_resolution.rs` - `BondSlashChoice`, `Payload::BondResolution` mapping, wire tests
- `src/ui/dispute_finalization_popup.rs` - Popup rendering logic
- `src/util/order_utils/execute_admin_settle.rs` - AdminSettle implementation (payload wiring pending)
- `src/util/order_utils/execute_admin_cancel.rs` - AdminCancel implementation (payload wiring pending)
- `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
- `src/ui/disputes_in_progress_tab.rs` - Main disputes UI with chat interface
- `src/ui/key_handler/enter_handlers.rs` - Enter key handling and chat message sending
Expand Down
10 changes: 5 additions & 5 deletions docs/MESSAGE_FLOW_AND_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -579,10 +579,10 @@ Database operations (saving orders, updating trade indices) log errors but don't

Admins resolve in-progress disputes by sending encrypted DMs signed with `admin_privkey` ([FINALIZE_DISPUTES.md](FINALIZE_DISPUTES.md)).

| Action | Trade outcome | Typical payload today | Planned payload |
|--------|---------------|----------------------|-----------------|
| `AdminSettle` | Pay buyer (release escrow to buyer) | `null` | `Payload::BondResolution` via [`BondSlashChoice`](../src/util/order_utils/bond_resolution.rs) |
| `AdminCancel` | Refund seller | `null` | same |
| Action | Trade outcome | Payload (via `BondSlashChoice`) |
|--------|---------------|--------------------------------|
| `AdminSettle` | Pay buyer (release escrow to buyer) | `None` → `null`; slash variants → `BondResolution` |
| `AdminCancel` | Refund seller | same |

**Bond resolution** (Mostro anti-abuse bond Phase 2+): optional `bond_resolution: { slash_seller, slash_buyer }` on both actions only. Four combinations plus legacy `null` (= no slash). See [admin settle](https://mostro.network/protocol/admin_settle_order.html) / [admin cancel](https://mostro.network/protocol/admin_cancel_order.html).

Expand All @@ -591,7 +591,7 @@ Admins resolve in-progress disputes by sending encrypted DMs signed with `admin_
- **Errors**: invalid slash (e.g. no bond for that side) → `CantDo(InvalidPayload)` → user string from [`get_cant_do_description`](../src/util/types.rs).
- **Post-slash payout**: slashed bonds may trigger `Action::AddBondInvoice` to the non-slashed party (daemon PR [#738](https://github.com/MostroP2P/mostro/pull/738)); handled on the trader notification path, not admin UI.

**Entry points (today):** `execute_finalize_dispute` → `execute_admin_settle` / `execute_admin_cancel` in `src/util/order_utils/`.
**Entry points:** `execute_finalize_dispute(dispute_id, bond, …)` → `execute_admin_settle` / `execute_admin_cancel`. UI still passes `BondSlashChoice::default()` until the slash picker lands.

## Stateless Recovery

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** on `admin-settle` / `admin-cancel` (`BondSlashChoice` + `mostro-core` 0.11.3 — protocol helpers done, TUI/execute wiring pending)
- **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)

## Contributing & tooling

Expand Down
12 changes: 9 additions & 3 deletions src/ui/key_handler/admin_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use crate::shared::permissions::SolverPermission;
use crate::ui::key_handler::EnterKeyContext;
use crate::ui::{AddSolverState, AdminMode, AppState, UiMode};
use crate::util::fatal::request_fatal_restart;
use crate::util::order_utils::{execute_admin_add_solver, execute_finalize_dispute};
use crate::util::order_utils::{
execute_admin_add_solver, execute_finalize_dispute, BondSlashChoice,
};
use uuid::Uuid;

use crate::ui::helpers::hydrate_app_admin_keys_from_privkey;
Expand Down Expand Up @@ -136,6 +138,7 @@ pub(crate) fn execute_finalize_dispute_action(
dispute_id: Uuid,
ctx: &EnterKeyContext<'_>,
is_settle: bool, // true = AdminSettle (pay buyer), false = AdminCancel (refund seller)
bond: BondSlashChoice,
) {
let Some(admin_keys) = ctx.admin_chat_keys.cloned() else {
app.mode = UiMode::OperationResult(OperationResult::Error(
Expand All @@ -162,6 +165,7 @@ pub(crate) fn execute_finalize_dispute_action(
tokio::spawn(async move {
match execute_finalize_dispute(
&dispute_id,
bond,
&admin_keys,
&client_clone,
current_mostro_pubkey,
Expand All @@ -178,8 +182,10 @@ pub(crate) fn execute_finalize_dispute_action(
"canceled (seller refunded)"
};
let _ = result_tx.send(OperationResult::Info(format!(
"✅ Dispute {} {}!",
dispute_id, action_name
"✅ Dispute {} {} ({})!",
dispute_id,
action_name,
bond.log_context()
)));
}
Err(e) => {
Expand Down
34 changes: 19 additions & 15 deletions src/ui/key_handler/enter_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,27 @@ use std::collections::HashSet;
use std::str::FromStr;

use crate::settings::load_settings_from_disk;
use crate::ui::key_handler::admin_handlers::{
execute_finalize_dispute_action, execute_take_dispute_action, handle_enter_admin_mode,
};
use crate::ui::key_handler::confirmation::{
create_key_input_state, handle_confirmation_enter, handle_input_to_confirmation,
};
use crate::ui::key_handler::message_handlers::submit_add_invoice;
use crate::ui::key_handler::message_handlers::{
handle_enter_message_notification, handle_enter_rating_order, handle_enter_viewing_message,
submit_add_invoice,
};
use crate::ui::key_handler::settings::{
clear_currency_filters, clear_ln_address_from_settings, handle_mode_switch,
save_currency_to_settings, save_mostro_pubkey_to_settings, save_relay_to_settings,
validate_ln_address_format,
};
use crate::ui::key_handler::validation::{
validate_currency, validate_mostro_pubkey, validate_relay,
};
use crate::ui::tabs::settings_tab::{settings_action_for_index, SettingsMenuAction};
use crate::util::dm_utils::{apply_saved_ln_address_invoice_choice, present_add_invoice_popup};
use crate::util::order_utils::BondSlashChoice;

fn invoice_popup_action_for_message_action(action: &Action) -> Option<Action> {
match action {
Expand All @@ -56,9 +66,6 @@ fn invoice_popup_action_for_message_action(action: &Action) -> Option<Action> {
_ => None,
}
}
use crate::ui::key_handler::validation::{
validate_currency, validate_mostro_pubkey, validate_relay,
};

fn generate_mnemonic_12_words() -> std::result::Result<String, String> {
Mnemonic::generate(12)
Expand Down Expand Up @@ -359,16 +366,6 @@ fn handle_enter_user_order_chat(app: &mut AppState, ctx: &super::EnterKeyContext
);
}

// Admin handlers moved to admin_handlers.rs
use crate::ui::key_handler::admin_handlers::{
execute_finalize_dispute_action, execute_take_dispute_action, handle_enter_admin_mode,
};

// Message handlers moved to message_handlers.rs
use crate::ui::key_handler::message_handlers::{
handle_enter_message_notification, handle_enter_rating_order, handle_enter_viewing_message,
};

/// Handle Enter key - dispatches to mode-specific handlers
pub fn handle_enter_key(app: &mut AppState, ctx: &super::EnterKeyContext<'_>) -> bool {
let default_mode = match app.user_role {
Expand Down Expand Up @@ -681,7 +678,14 @@ pub fn handle_enter_key(app: &mut AppState, ctx: &super::EnterKeyContext<'_>) ->
}) => {
if selected_button {
// YES selected - execute the finalization action
execute_finalize_dispute_action(app, dispute_id, ctx, is_settle);
// TODO(bond-slash UI): pass admin-selected bond; default preserves legacy null payload.
execute_finalize_dispute_action(
app,
dispute_id,
ctx,
is_settle,
BondSlashChoice::default(),
);
} else {
// NO selected - go back to finalization popup
app.mode = UiMode::AdminMode(AdminMode::ReviewingDisputeForFinalization {
Expand Down
10 changes: 10 additions & 0 deletions src/util/order_utils/bond_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ impl BondSlashChoice {
_ => Some(self.to_payload()),
}
}

/// Short phrase for admin finalization log lines.
pub fn log_context(self) -> &'static str {
match self {
Self::None => "no bond slash",
Self::SlashBuyer => "slash buyer bond",
Self::SlashSeller => "slash seller bond",
Self::SlashBoth => "slash both bonds",
}
}
}

#[cfg(test)]
Expand Down
35 changes: 29 additions & 6 deletions src/util/order_utils/execute_admin_cancel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use mostro_core::prelude::*;
use nostr_sdk::prelude::*;
use uuid::Uuid;

use super::BondSlashChoice;
use crate::util::dm_utils::send_dm;
use crate::util::mostro_info::MostroInstanceInfo;

Expand All @@ -20,6 +21,7 @@ use crate::util::mostro_info::MostroInstanceInfo;
/// # Arguments
///
/// * `order_id` - The UUID of the order associated with this dispute (Mostro expects this ID)
/// * `bond` - Anti-abuse bond slash choice (`to_optional_payload()` on the wire)
/// * `client` - The Nostr client for sending messages
/// * `mostro_pubkey` - The public key of the Mostro daemon
///
Expand All @@ -37,19 +39,18 @@ use crate::util::mostro_info::MostroInstanceInfo;
/// - Failed to send the DM
pub async fn execute_admin_cancel(
order_id: &Uuid,
bond: BondSlashChoice,
admin_keys: &Keys,
client: &Client,
mostro_pubkey: PublicKey,
mostro_instance: Option<&MostroInstanceInfo>,
) -> Result<()> {
// Create AdminCancel message (order UUID in `id`; dispute UUID is local-only).
// TODO(bond-slash): accept `BondSlashChoice` and pass `bond.to_optional_payload()` here.
let payload = bond.to_optional_payload();
let cancel_message =
Message::new_dispute(Some(*order_id), None, None, Action::AdminCancel, None)
Message::new_dispute(Some(*order_id), None, None, Action::AdminCancel, payload)
.as_json()
.map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

// Send the DM using admin keys (signed gift wrap)
send_dm(
client,
Some(admin_keys),
Expand All @@ -63,8 +64,30 @@ pub async fn execute_admin_cancel(
.await?;

log::info!(
"✅ Admin cancel (refund seller) message sent for order {}",
order_id
"✅ Admin cancel (refund seller) message sent for order {} ({})",
order_id,
bond.log_context()
);
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use uuid::uuid;

#[test]
fn admin_cancel_none_omits_payload() {
let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
let msg = Message::new_dispute(
Some(order_id),
None,
None,
Action::AdminCancel,
BondSlashChoice::None.to_optional_payload(),
);
assert!(msg.verify());
let json = msg.as_json().expect("serialize");
assert!(json.contains("\"payload\":null"));
}
}
Loading
Loading