Admin Dispute Finalization

June 18, 2026 Β· View on GitHub

Overview

This document describes how admins finalize disputes in Mostrix after reviewing the case and communicating with the buyer and seller.

Implementation status (anti-abuse bond slash)

LayerStatusNotes
mostro-core 0.13.0DoneBondResolution, Payload::BondResolution, CantDoReason::InvalidPayload, Status::WaitingMakerBond, Transport
BondSlashChoiceDonesrc/util/order_utils/bond_resolution.rs β€” wire mapping + unit tests
Execute layer (execute_admin_settle / cancel)Donerequest_id + wait_for_dm + handle_mostro_response; expects AdminSettled / AdminCanceled; CantDo before DB update
Success / error popupDoneBondSlashChoice::finalize_success_message; word-wrapped OperationResult::Info in operation_result.rs
TUI (slash picker + confirm summary)DoneInline bond button + overlay; confirm shows bond.label() recap
bond_enabled gating (kind 38385)DoneParse tag in mostro_info.rs; hide Bond button when not "true"
Trader AddBondInvoiceDonePayout popup + execute_add_bond_invoice; wait_for_dm + follow-up via OpenInvoicePopup / PaymentRequestRequired (MESSAGE_FLOW_AND_PROTOCOL.md)

Protocol references: Admin Settle, Admin Cancel.

User Flow

  1. Navigate to Disputes: Admin opens the "Disputes in Progress" tab
  2. Select Dispute: Use Up/Down arrows to select a dispute from the left sidebar
  3. Review Details: View dispute information in the header (parties, amounts, ratings, privacy)
  4. Chat with Parties:
    • Use Tab to switch between buyer and seller chat views
    • Press Shift+I to enable/disable chat input (prevents accidental typing)
    • Type messages directly in the input box (when input enabled)
    • Press Enter to send messages
    • Use PageUp/PageDown to scroll through chat history
    • Press End to jump to bottom of chat (latest messages)
    • 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 actions on one popup (Left/Right): πŸ’° Pay buyer (AdminSettle), ↩️ Refund seller (AdminCancel), and Bond when the instance advertises bond_enabled: true on kind 38385 (otherwise a two-button layout only). Button titles use the outcome labels; the body shows protocol names Admin settle / Admin cancel (and bond.label() on Bond). 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 β€” UI enters AdminMode::WaitingDisputeFinalization, sends encrypted DM to Mostro, waits for reply.
  11. Result: Success β†’ multi-line Operation Successful popup; failure (timeout, CantDo, wrong action) β†’ Operation Failed with daemon text. Esc / Enter closes; disputes list refreshes on success ("Dispute finalized" marker in main.rs).

Current UI: single finalize popup (7–8) β†’ confirm with bond recap when bonds enabled (9) β†’ wait + result (10–11).

Finalization Actions

Pay Buyer (AdminSettle)

  • Protocol Action: Action::AdminSettle
  • Effect: Settles the dispute in favor of the buyer
  • Result: Buyer receives the full amount from escrow
  • Use Case: When buyer's claim is valid (e.g., seller didn't deliver, scam attempt)

Refund Seller (AdminCancel)

  • Protocol Action: Action::AdminCancel
  • Effect: Cancels the order and refunds the seller
  • Result: Seller receives the full amount back from escrow
  • Use Case: When seller's position is valid (e.g., buyer false claim, buyer unresponsive)

Exit

  • 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.

ChoiceTUI label (label())slash_sellerslash_buyerWhen to use
No bond slashπŸ”“ No bond slashfalsefalseRelease bonds; no penalty
Slash buyer bondβš”οΈ Slash buyer bondfalsetrueBuyer at fault (e.g. false claim on sell order)
Slash seller bondβš”οΈ Slash seller bondtruefalseSeller at fault
Slash both bondsβš”οΈ Slash both bondstruetrueBoth parties violated rules

Mostrix maps these via BondSlashChoice: 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).

If the daemon rejects a slash (e.g. side has no bond row), Mostro may reply with CantDo(InvalidPayload) β€” surfaced as "Invalid payload - check bond slash choices or message format" (get_cant_do_description).

After a slash, the non-slashed party may receive Action::AddBondInvoice (Payload::BondPayoutRequest) to claim their counterparty share; Mostrix opens the bond payout invoice popup (not the admin finalization popup). When they submit their bolt11, execute_add_bond_invoice waits for Mostro’s next DM and chains into the normal trade flowβ€”for example waiting-buyer-invoice on a sell take opens the Add Invoice popup for the buyer/taker. See the bond payout submit table in MESSAGE_FLOW_AND_PROTOCOL.md.

Instance bond_enabled (kind 38385)

Mostro always emits a bond_enabled tag ("true" / "false"). Mostrix reads it via instance_bonds_enabled(): only "true" shows the Bond button and confirm bond recap. The same instance event may include protocol_version ("1" / "2") for wire transport discovery β€” see MESSAGE_FLOW_AND_PROTOCOL.md. Fetch instance info from the Mostro Info tab (Enter) so gating reflects the connected daemon.

UI Components

Finalization Popup

The popup displays comprehensive dispute information:

Header Section:

  • Order ID (full UUID) - the order associated with this dispute
  • Dispute ID (full UUID) - the unique dispute identifier
  • Dispute type and status
  • Creation and Taken timestamps

Note: The UI displays both Order ID and Dispute ID. Previous documentation only mentioned "Dispute ID (full UUID)" which was incomplete. βœ… Resolved in this PR.

Parties Section:

  • Buyer information: pubkey (truncated), role indicator (🟒 BUYER), privacy status ("Privacy: Yes/No"), rating with operating days
  • Seller information: pubkey (truncated), role indicator (πŸ”΄ SELLER), privacy status ("Privacy: Yes/No"), rating with operating days
  • Initiator indicator (shows "(Initiator)" suffix on the party who started the dispute)

Note: Privacy status is displayed as text labels "Yes" or "No" (not emoji indicators). The emojis (🟒/πŸ”΄) are used for role indicators (BUYER/SELLER), not privacy. Previous documentation described "privacy status (🟒 info available / πŸ”΄ private)" which was incorrect. βœ… Resolved in this PR.

Financial Section:

  • Amount in satoshis
  • Fiat amount with currency code (e.g., "1000 USD")
  • Premium percentage
  • Payment method (if available)

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 (three columns, Left/Right focus):

Button (title bar)Inner body (active)When selectedEnter
πŸ’° Pay buyerAdmin settleGreen highlightOpen confirm (settle)
↩️ Refund sellerAdmin cancelRed highlightOpen confirm (cancel)
Bondbond.label()Primary highlightOpen bond overlay submenu

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

Keyboard Navigation

In Dispute List:

  • Up/Down: Select dispute in sidebar
  • Tab: Switch between buyer/seller chat party
  • Shift+I: Toggle chat input enabled/disabled
  • Type: Start typing message in input box (when input enabled)
  • Enter: Send message
  • Shift+F: Open finalization popup
  • PageUp/PageDown: Scroll through chat history
  • End: Jump to bottom of chat (latest messages)
  • Backspace: Delete characters from input (when input enabled)

In Finalization Popup:

  • 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

Message Structure

Both finalization actions use Message::new_dispute with the order UUID (from admin_disputes.id), not the dispute UUID:

use crate::util::order_utils::BondSlashChoice;

let bond = BondSlashChoice::SlashBuyer; // example

Message::new_dispute(
    Some(order_id),
    None,
    None,
    Action::AdminSettle, // or AdminCancel
    bond.to_optional_payload(), // None β†’ null; slash variants β†’ BondResolution
)

Example JSON (settle + slash buyer), per protocol:

{
  "dispute": {
    "version": 1,
    "id": "<order-uuid>",
    "action": "admin-settle",
    "payload": {
      "bond_resolution": {
        "slash_seller": false,
        "slash_buyer": true
      }
    }
  }
}

Note: Mostrix serializes Message::Dispute (not the order wrapper shown in some protocol examples); mostro-core accepts both shapes on decode. The id field is always the order id.

Internally, Mostrix:

  • Looks up the dispute in the local admin_disputes table by its dispute_id.
  • 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 β€” spawns async task with bond from ConfirmFinalizeDispute (chosen on the finalize popup / overlay).
  2. execute_finalize_dispute β€” DB guards, then dispatches settle or cancel with the same bond.
  3. execute_admin_settle / execute_admin_cancel β€” Message::new_dispute(..., bond.to_optional_payload()) with request_id, wait_for_dm, and handle_mostro_response (surfaces CantDo before DB update). Success requires AdminSettled / AdminCanceled from Mostro.

Success popup (OperationResult::Info) is built by BondSlashChoice::finalize_success_message and rendered with newline-aware, word-boundary wrapping in operation_result.rs (dynamic popup height).

Example layout:

Dispute finalized

Outcome:
Admin cancel β€” seller refunded

Bond:
βš”οΈ Slash buyer bond

Dispute ID:
8397bc78-7c98-4b4f-bb49-40c7101391b0

(Settle uses Admin settle β€” buyer paid.)

UI state (AdminMode)

Finalize flow uses a single ReviewingDisputeForFinalization 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 carries is_settle, bond, selected_button (Yes/No).

Confirmation popup

Rendered by 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
  • Sent via encrypted DM to Mostro daemon
  • Admin keys must be configured in settings.toml

Expected Responses

After sending a finalization action, Mostro replies over the same admin DM channel:

RequestSuccess actionFailure
AdminSettleAdminSettledCantDo (e.g. InvalidPayload for bad bond slash)
AdminCancelAdminCanceledsame

Mostrix waits with wait_for_dm and validates via handle_mostro_response before updating admin_disputes.

Database Updates

After successful finalization:

  1. Dispute status updated in local database
  2. Dispute may be moved to "resolved" list
  3. Local dispute cache refreshed

Error Handling

Possible error scenarios:

  • Mostro daemon unresponsive or wait_for_dm timeout (15s)
  • Invalid admin credentials
  • Dispute already finalized (blocked before send)
  • Network/relay issues
  • Dispute not found (e.g., dispute was removed or ID is invalid)
  • CantDo from Mostro (e.g. InvalidPayload for impossible bond slash) β€” surfaced via handle_mostro_response / get_cant_do_description; local DB is not updated
  • Unexpected response action (not AdminSettled / AdminCanceled)
  • Data integrity error: Missing required fields (buyer_pubkey or seller_pubkey)

Errors use the same word-wrapped Operation Failed popup as other flows (operation_result.rs). The finalization popup includes robust error handling:

  • Dispute Not Found: If a dispute ID is invalid or the dispute is no longer available, a clear error popup is displayed with the dispute ID and instructions to close it (Press ESC or ENTER).
  • Data Integrity Error: If a dispute is missing required fields (buyer_pubkey or seller_pubkey), a dedicated error popup is displayed explaining that the database entry is incomplete and the dispute cannot be finalized. This validation happens both when taking a dispute (prevents saving incomplete data) and when viewing the finalization popup.
  • User-Friendly Messages: All error messages are descriptive and help users understand what went wrong.
  • Safe Display: Dispute IDs and other data are safely truncated to prevent display issues with unexpected data lengths.

Source: src/ui/dispute_finalization_popup.rs:22, src/models.rs (AdminDispute::new validation)

Chat System

Features

The chat interface provides real-time communication with dispute parties:

Visual Design:

  • Color-coded senders: Each message displays a header in the format "Sender - date - time" where the sender name is color-coded:
    • Cyan: Admin messages
    • Green: Buyer messages
    • Red: Seller messages
  • Dynamic input box: Automatically grows from 1 to 10 lines based on message length
  • Focus indicators: Bold yellow border when typing, gray when inactive
  • Chat history: Scrollable message history per dispute

Message Management:

  • Per-dispute storage: Each dispute has its own chat history (stored in admin_dispute_chats)
  • Party filtering: Messages are filtered by the active chat party:
    • Admin messages: Only shown in the chat view of the party they were sent to (tracked via target_party field)
    • Buyer messages: Only shown when viewing the Buyer chat
    • Seller messages: Only shown when viewing the Seller chat
  • Scroll control:
    • PageUp/PageDown to navigate history
    • End key to jump to bottom (latest messages)
    • Visual scrollbar on the right shows position (↑/↓/β”‚/β–ˆ symbols)
    • Auto-scrolls to newest after sending
  • Empty state: Shows "No messages yet" when starting a new conversation

Input Handling:

  • Input toggle: Press Shift+I to enable/disable chat input
    • When disabled, prevents accidental typing while navigating
    • Visual indicator in input title shows enabled/disabled state
    • Input is enabled by default when entering dispute management
  • Text wrapping: Input wraps at word boundaries, respects available width
  • Character limit: Grows up to 10 lines, with visual feedback
  • Send behavior: Enter sends message, Shift+F opens finalization popup
  • Clear on send: Input automatically clears after sending

The footer shows context-sensitive shortcuts:

When typing (input enabled):

Tab: Switch Party | Enter: Send | Shift+I: Disable | Shift+F: Finalize | PgUp/PgDn: Scroll | End: Bottom | ↑↓: Select Dispute

When typing (input disabled):

Tab: Switch Party | Shift+I: Enable | Shift+F: Finalize | PgUp/PgDn: Scroll | ↑↓: Navigate Chat | End: Bottom | ↑↓: Select Dispute

When not typing:

Tab: Switch Party | Shift+F: Finalize | ↑↓: Select Dispute | PgUp/PgDn: Scroll Chat | End: Bottom

Best Practices

  1. Always chat first: Communicate with both parties before finalizing
  2. Review all evidence: Check chat history, payment proofs, timestamps
  3. Consider reputation: Factor in user ratings and operating days (shown in header)
  4. Document reasoning: All chat messages are stored per dispute for review
  5. Be impartial: Base decisions on facts, not party behavior alone
  6. Check privacy: Privacy labels ("Yes" = private mode / "No" = public mode) indicate whether user info may be limited
  7. Switch parties: Use Tab to alternate between buyer and seller chats
  8. Scroll history: Use PageUp/PageDown to review full conversation history, or End to jump to latest
  9. Toggle input: Use Shift+I to disable input when navigating to prevent accidental typing
  10. Monitor scrollbar: Visual scrollbar on the right shows your position in the chat history
  • src/util/order_utils/bond_resolution.rs - BondSlashChoice, wire mapping, finalize_success_message
  • src/ui/dispute_finalization_popup.rs - Finalize popup (titles + inner Admin settle / Admin cancel / bond label)
  • src/ui/operation_result.rs - Success/error popups (word wrap, dynamic height for Info)
  • src/ui/key_handler/admin_handlers.rs - execute_finalize_dispute_action, waiting mode, result channel
  • 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; waits for AdminSettled
  • src/util/order_utils/execute_admin_cancel.rs - AdminCancel; waits for AdminCanceled
  • src/util/order_utils/execute_finalize_dispute.rs - DB checks + dispatches settle/cancel
  • src/util/order_utils/execute_add_invoice.rs - execute_add_invoice, execute_add_bond_invoice / execute_bond_payment_request_reply
  • src/util/dm_utils/notifications_ch_mng.rs - apply_open_invoice_popup_from_execute, present_add_invoice_popup
  • 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
  • src/ui/key_handler/mod.rs - Chat input handling and clipboard operations
  • src/ui/mod.rs - AppState with chat storage (DisputeChatMessage, ChatSender)
  • src/models.rs - AdminDispute data model

See Also