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)
| Layer | Status | Notes |
|---|---|---|
mostro-core 0.13.0 | Done | BondResolution, Payload::BondResolution, CantDoReason::InvalidPayload, Status::WaitingMakerBond, Transport |
BondSlashChoice | Done | src/util/order_utils/bond_resolution.rs β wire mapping + unit tests |
Execute layer (execute_admin_settle / cancel) | Done | request_id + wait_for_dm + handle_mostro_response; expects AdminSettled / AdminCanceled; CantDo before DB update |
| Success / error popup | Done | BondSlashChoice::finalize_success_message; word-wrapped OperationResult::Info in operation_result.rs |
| TUI (slash picker + confirm summary) | Done | Inline bond button + overlay; confirm shows bond.label() recap |
bond_enabled gating (kind 38385) | Done | Parse tag in mostro_info.rs; hide Bond button when not "true" |
Trader AddBondInvoice | Done | Payout 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
- Navigate to Disputes: Admin opens the "Disputes in Progress" tab
- Select Dispute: Use Up/Down arrows to select a dispute from the left sidebar
- Review Details: View dispute information in the header (parties, amounts, ratings, privacy)
- 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
- Open Finalization: Press Shift+F to open finalization popup
- Review Full Details: Popup shows complete dispute information
- Choose actions on one popup (Left/Right): π° Pay buyer (
AdminSettle), β©οΈ Refund seller (AdminCancel), and Bond when the instance advertisesbond_enabled: trueon kind 38385 (otherwise a two-button layout only). Button titles use the outcome labels; the body shows protocol namesAdmin settle/Admin cancel(andbond.label()on Bond). Esc closes the popup (no separate Exit button). - Bond slash submenu (optional): With Bond focused, Enter opens overlay βοΈ Bond resolution; β/β among four labeled choices; Enter applies; Esc closes submenu only.
- Confirm: Enter on pay/refund opens Yes/No β title e.g.
β οΈ Confirm π° Pay buyer, description + Bond: recap (bond.label()). - Execute: Enter on Yes β UI enters
AdminMode::WaitingDisputeFinalization, sends encrypted DM to Mostro, waits for reply. - 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 inmain.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.
| 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: 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 selected | Enter |
|---|---|---|---|
| π° Pay buyer | Admin settle | Green highlight | Open confirm (settle) |
| β©οΈ Refund seller | Admin cancel | Red highlight | Open confirm (cancel) |
| Bond | bond.label() | Primary highlight | Open 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 theorderwrapper shown in some protocol examples);mostro-coreaccepts both shapes on decode. Theidfield is always the order id.
Internally, Mostrix:
- Looks up the dispute in the local
admin_disputestable by its dispute_id. - Reads the corresponding order ID from the
idcolumn. - 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):
execute_finalize_dispute_actionβ spawns async task withbondfromConfirmFinalizeDispute(chosen on the finalize popup / overlay).execute_finalize_disputeβ DB guards, then dispatches settle or cancel with the samebond.execute_admin_settle/execute_admin_cancelβMessage::new_dispute(..., bond.to_optional_payload())withrequest_id,wait_for_dm, andhandle_mostro_response(surfacesCantDobefore DB update). Success requiresAdminSettled/AdminCanceledfrom 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(defaultNone)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:
| Request | Success action | Failure |
|---|---|---|
AdminSettle | AdminSettled | CantDo (e.g. InvalidPayload for bad bond slash) |
AdminCancel | AdminCanceled | same |
Mostrix waits with wait_for_dm and validates via handle_mostro_response before updating admin_disputes.
Database Updates
After successful finalization:
- Dispute status updated in local database
- Dispute may be moved to "resolved" list
- Local dispute cache refreshed
Error Handling
Possible error scenarios:
- Mostro daemon unresponsive or
wait_for_dmtimeout (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)
CantDofrom Mostro (e.g.InvalidPayloadfor impossible bond slash) β surfaced viahandle_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_pubkeyorseller_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_partyfield) - Buyer messages: Only shown when viewing the Buyer chat
- Seller messages: Only shown when viewing the Seller chat
- Admin messages: Only shown in the chat view of the party they were sent to (tracked via
- 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
Chat Footer
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
- Always chat first: Communicate with both parties before finalizing
- Review all evidence: Check chat history, payment proofs, timestamps
- Consider reputation: Factor in user ratings and operating days (shown in header)
- Document reasoning: All chat messages are stored per dispute for review
- Be impartial: Base decisions on facts, not party behavior alone
- Check privacy: Privacy labels ("Yes" = private mode / "No" = public mode) indicate whether user info may be limited
- Switch parties: Use Tab to alternate between buyer and seller chats
- Scroll history: Use PageUp/PageDown to review full conversation history, or End to jump to latest
- Toggle input: Use Shift+I to disable input when navigating to prevent accidental typing
- Monitor scrollbar: Visual scrollbar on the right shows your position in the chat history
Related Files
src/util/order_utils/bond_resolution.rs-BondSlashChoice, wire mapping,finalize_success_messagesrc/ui/dispute_finalization_popup.rs- Finalize popup (titles + innerAdmin settle/Admin cancel/ bond label)src/ui/operation_result.rs- Success/error popups (word wrap, dynamic height forInfo)src/ui/key_handler/admin_handlers.rs-execute_finalize_dispute_action, waiting mode, result channelsrc/ui/dispute_bond_slash_popup.rs-render_bond_slash_overlayon finalize popupsrc/ui/dispute_finalization_confirm.rs- Yes/No confirm with bond recapsrc/util/order_utils/execute_admin_settle.rs- AdminSettle; waits forAdminSettledsrc/util/order_utils/execute_admin_cancel.rs- AdminCancel; waits forAdminCanceledsrc/util/order_utils/execute_finalize_dispute.rs- DB checks + dispatches settle/cancelsrc/util/order_utils/execute_add_invoice.rs-execute_add_invoice,execute_add_bond_invoice/execute_bond_payment_request_replysrc/util/dm_utils/notifications_ch_mng.rs-apply_open_invoice_popup_from_execute,present_add_invoice_popupsrc/ui/disputes_in_progress_tab.rs- Main disputes UI with chat interfacesrc/ui/key_handler/enter_handlers.rs- Enter key handling and chat message sendingsrc/ui/key_handler/mod.rs- Chat input handling and clipboard operationssrc/ui/mod.rs- AppState with chat storage (DisputeChatMessage, ChatSender)src/models.rs- AdminDispute data model
See Also
- ADMIN_DISPUTES.md - Admin dispute management overview
- MESSAGE_FLOW_AND_PROTOCOL.md - Mostro protocol details
- TUI_INTERFACE.md - General UI navigation