Admin Dispute Resolution
July 27, 2026 Β· View on GitHub
This guide explains the admin mode functionality for dispute resolution in Mostrix. Admin mode allows authorized users to resolve disputes between buyers and sellers on the Mostro network.
Admin Mode Overview
Admin mode can be activated in two ways:
- On startup (default mode): set
user_mode = "admin"and configure a validadmin_privkeyinsettings.toml. - At runtime (TUI switch): start in user mode and press
Min the Settings tab to toggle between User and Admin modes. The selected mode is persisted back tosettings.toml(user_modefield).
Only the admin private key can be used to sign dispute resolution actions.
Source: src/settings.rs:12
pub admin_privkey: String,
Admin Tabs
The admin interface provides dedicated tabs for dispute management:
1. Disputes Pending Tab
Lists all pending disputes on the Mostro network (state: Initiated). Admins can:
- View dispute details: Order ID, parties involved, status
- Take a dispute: Select a dispute and press Enter to take ownership
- Navigate: Use arrow keys to browse the dispute list
- Color coding: Disputes are color-coded by status (Yellow for pending)
2. Disputes in Progress Tab
Status: β Fully Implemented with Complete Chat System
Shows disputes that have been taken by this admin (state: InProgress). This is the primary workspace for resolving disputes.
Layout
The interface is divided into three main sections:
-
Left Sidebar (20%): List of disputes in progress
- Shows truncated dispute IDs (safely handles short IDs without panicking)
- Highlighted selection with Up/Down arrow keys
- Updates main area when selection changes
- Shows "No disputes in progress" when empty
-
Main Area (80%):
- Empty State: When no disputes are available, displays "Select a dispute from the sidebar" with a footer showing key hints (filter +
ββ: Select Dispute | Ctrl+H: Help). The footer is width-aware and always includes Ctrl+H for the help popup. - Header (7 lines): Comprehensive dispute information
- Dispute ID, Type, Status
- Creation date and timestamps
- Initiator role (Buyer or Seller with pubkey)
- Privacy indicators (π’ info available / π΄ private)
- Amount in sats and fiat
- User ratings with operating days
- Party Tabs (3 lines): Buyer/Seller chat selection buttons
- Green "BUYER" button with truncated pubkey
- Red "SELLER" button with truncated pubkey
- Tab key switches between parties
- Chat Area (flexible): Scrollable message history
- Color-coded messages (Cyan=Admin, Green=Buyer, Magenta=Seller)
- PageUp/PageDown scrolling
- Shows "No messages yet" when empty
- Input Box (dynamic 1-10 lines): Message composition
- Grows automatically based on content
- Yellow bold border when focused
- Text wrapping with word boundaries
- Footer (1β2 lines): Context-sensitive keyboard shortcuts. The footer is width-aware: on very narrow terminals it shows only Ctrl+H: Help; on medium width it shows essential keys plus Ctrl+H; on wide terminals it can render on two lines with the full list. All variants include Ctrl+H: Help to open the context-aware shortcuts popup.
- Empty State: When no disputes are available, displays "Select a dispute from the sidebar" with a footer showing key hints (filter +
Dispute Management Features
- Real-time chat: Direct typing with instant visual feedback
- 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 for the finalize popup (π° pay buyer / β©οΈ refund seller bodies show Admin settle / Admin cancel; optional bond slash when instance
bond_enabledis true on kind 38385; Esc to close). Confirm shows bond recap when bonds are enabled. Execute:execute_finalize_disputeβexecute_admin_settle/execute_admin_cancel(request_id,wait_for_dm,handle_mostro_responseforCantDo); success toast viaBondSlashChoice::finalize_success_message. See FINALIZE_DISPUTES.md.
- Finalization: Press Shift+F for the finalize popup (π° pay buyer / β©οΈ refund seller bodies show Admin settle / Admin cancel; optional bond slash when instance
- Visual indicators: Focus states, colors, and icons for clarity
Keyboard Navigation
- Up/Down: Select dispute in sidebar
- Tab: Switch between buyer and seller chat
- Type: Start composing message (when input enabled)
- Enter: Send message (when input has text)
- Shift+F: Open finalization popup for the selected dispute
- PageUp/PageDown: Scroll chat history
- End: Jump to bottom of chat (latest messages)
- Shift+I: Toggle chat input enabled/disabled
- Backspace: Delete characters (when input enabled)
- Ctrl+H: Open help popup with all shortcuts for this tab (Esc/Enter/Ctrl+H to close)
See FINALIZE_DISPUTES.md for detailed finalization workflow.
3. Observer Tab
Status: β Implemented β Relay-based chat inspection
The Observer tab is a read-only tool that lets admins inspect encrypted user-to-user chats by fetching NIP-59 gift-wrap messages from Nostr relays. A party involved in a dispute provides the admin with their shared key (64-char hex) out-of-band so the admin can view the full conversation and decide who is right.
Observer Workflow
- Acquire the shared key:
- During a dispute, one of the parties provides the admin with their shared key as a 64-character hex string (32-byte ECDH secret derived between the two trade pubkeys).
- Open Observer tab:
- Switch to Admin mode and navigate to the Observer tab.
- Paste shared key:
- In the "Shared key (64-char hex)" input, paste the key provided by the user. The input supports bracketed paste mode for seamless pasting.
- Fetch and view chat:
- Press Enter to:
- Validate the shared key (non-empty, valid hex).
- Derive
Keysfrom the shared key hex (viakeys_from_shared_hexinchat_utils.rs). - Fetch all
Kind::GiftWrapevents addressed to the shared key's public key from the last 7 days (reusesfetch_gift_wraps_for_shared_key). - Decrypt each event using the shared key (standard NIP-59 or simplified Mostro-chat format).
- Map sender public keys to roles: known admin pubkey = Admin, first unknown pubkey = Buyer, second unknown pubkey = Seller.
- Parse attachments (Mostro Mobile Encrypted File Messaging format:
image_encrypted/file_encrypted).
- The chat is displayed using the same rich formatting as the dispute chat: color-coded sender labels (Cyan=Admin, Green=Buyer, Magenta=Seller), timestamps, and attachment indicators.
- Press Enter to:
- Save attachments:
- Press Ctrl+S to open a save-attachment popup listing all file/image attachments found in the observer chat. Select with Up/Down, save with Enter, cancel with Esc. Files are saved to
~/.mostrix/downloads/observer_<key_prefix>_<filename>.
- Press Ctrl+S to open a save-attachment popup listing all file/image attachments found in the observer chat. Select with Up/Down, save with Enter, cancel with Esc. Files are saved to
Observer Keyboard Shortcuts
- Enter: Fetch chat from relays using the current shared key.
- Ctrl+C: Clear shared key input, messages, error state, and loading indicator. Sensitive data is securely cleared with
zeroize. - Ctrl+S: Open save-attachment popup (when attachments are present in the fetched chat).
- Ctrl+H: Open help popup with Observer shortcuts (Esc/Enter/Ctrl+H to close).
When validation or fetching fails (empty key, invalid hex, no messages found, decryption error), Observer sets an inline error message in the header and raises a shared OperationResult popup showing the failure reason. Closing this popup with Esc or Enter keeps the admin on the Observer tab so they can immediately fix the input and retry.
Observer State (AppState)
observer_shared_key_input: String-- current shared key input.observer_messages: Vec<DisputeChatMessage>-- fetched and decrypted chat messages.observer_loading: bool-- indicates an async fetch is in progress.observer_error: Option<String>-- inline error message.UiMode::ObserverSaveAttachmentPopup(usize)-- active when the save-attachment popup is open for observer messages.
The fetch is performed asynchronously via tokio::spawn calling chat_utils::fetch_observer_chat. Results are sent back to the main event loop through the order_result_tx channel using the OperationResult::ObserverChatLoaded and OperationResult::ObserverChatError variants.
When closing the operation result popup from the Disputes in Progress tab (e.g. after saving an attachment or after a finalization result), the app stays on Disputes in Progress and returns to ManagingDispute mode instead of switching to the first tab.
Note: Observer fetches messages from Nostr relays using the shared key. It reuses the same gift-wrap fetch infrastructure as the admin chat system (
fetch_gift_wraps_for_shared_keyinchat_utils.rs). Sensitive data (shared key, message contents) is securely cleared from memory viazeroizewhen the admin clears the observer state with Ctrl+C.
Source: src/ui/tabs/observer_tab.rs (rendering), src/ui/key_handler/enter_handlers.rs (Enter handler), src/util/chat_utils.rs (fetch_observer_chat), src/ui/key_handler/mod.rs (Ctrl+S and observer save-attachment popup handling), src/ui/save_attachment_popup.rs (render_observer_save_attachment_popup)
4. Settings Tab
Status: β Fully Implemented and Working
The Settings tab provides comprehensive configuration options for both User and Admin modes. The available options differ based on the current role.
User Mode Options
- Change Mostro Pubkey: Update the Mostro instance pubkey used by the client (hex format, 64 characters).
- Add Nostr Relay: Add a new Nostr relay to the relay list (must start with
wss://). Relays are added to the running client immediately. - Add Currency Filter: Adds fiat currency codes (e.g., USD, EUR) to the
currencies_filterarray insettings.toml. The fetch scheduler in Mostrix reloadscurrencies_filteron each tick and uses it to filter which orders are visible: only orders whose fiat code is in this list are shown. The list of available fiat currencies is still defined by the Mostro instance via thefiat_currencies_acceptedtag in its status event; the filter insettings.tomlonly narrows which of those are displayed. Same behaviour in Admin mode. - Clear Currency Filters: Clears the
currencies_filterarray insettings.toml. An empty list means no filter: all orders from the Mostro instance are shown again. The scheduler picks up the change on the next tick.
Admin Mode Options
- Change Mostro Pubkey: Update the Mostro instance pubkey used by the client (hex format, 64 characters).
- Add Nostr Relay: Add a new Nostr relay to the relay list (must start with
wss://). Relays are added to the running client immediately. - Add Currency Filter: Same as in User mode: adds codes to
currencies_filterinsettings.toml; the scheduler uses this to filter visible orders. Mostroβsfiat_currencies_accepteddefines which currencies the instance supports; Mostrix usescurrencies_filteronly to restrict which orders are shown. - Clear Currency Filters: Clears
currencies_filterinsettings.toml; the scheduler then shows all orders (no currency filter) on the next fetch. - Add Dispute Solver: Add a new dispute solver to the network (see Adding a Solver section).
- Change Admin Key: Update the admin private key used for signing dispute actions.
Settings Tab Features
- Mode Display: Shows current mode (User/Admin) at the top
- Mode Switching: Press
Mkey to switch between User and Admin modes - Confirmation Popups: All settings changes require confirmation before saving
- Input Validation: All inputs are validated before processing:
- Mostro pubkey: Must be 64-character hex string
- Relay URLs: Must start with
wss:// - Currency codes: Non-empty, max 10 characters
- Solver public key: Accepts
npub1...or 64-char hex public key - Admin private key: Accepts
nsec1...or 64-char hex secret key
- Keyboard Input: All inputs support both paste and keyboard entry
- Settings Persistence: All changes are saved to
settings.tomlfile - Error Handling: Invalid inputs display error popups with clear messages
- Dynamic Updates:
- Relays are added to the running Nostr client immediately
- Currency filters are applied in real-time to order fetching
- Status bar displays current settings (Mostro pubkey, relays, currencies)
Source: src/ui/settings_tab.rs, src/ui/key_handler/settings.rs, src/ui/key_handler/validation.rs
Dispute States
Disputes progress through different states during their lifecycle. Understanding these states helps admins know what actions are available and what the current status of a dispute is.
The dispute Status enum defines the possible states:
pub enum Status {
/// Dispute initiated and waiting to be taken by a solver
#[default]
Initiated,
/// Taken by a solver
InProgress,
/// Canceled by admin/solver and refunded to seller
SellerRefunded,
/// Settled seller's invoice by admin/solver and started to pay sats to buyer
Settled,
/// Released by the seller
Released,
}
State Descriptions
1. Initiated (Default)
- Meaning: The dispute has been created and is waiting for an admin/solver to take ownership.
- Admin Actions Available:
- Take the dispute (moves to
InProgress) - View dispute details
- Take the dispute (moves to
- Next State:
InProgress(when taken by admin)
2. InProgress
- Meaning: An admin/solver has taken ownership of the dispute and is actively working on resolution.
- Admin Actions Available:
- Communicate with buyer and seller
- Request additional information
- Resolve in favor of buyer (moves to
Settled) - Resolve in favor of seller (moves to
SellerRefunded)
- Next States:
Settled,SellerRefunded, orReleased
3. SellerRefunded
- Meaning: The dispute was resolved in favor of the seller. The seller has been refunded, and the buyer's payment was returned.
- Admin Actions Available:
- View dispute history (dispute is closed)
- Final State: No further actions possible
4. Settled
- Meaning: The admin/solver has settled the seller's invoice and started the process of paying sats to the buyer. This indicates resolution in favor of the buyer.
- Admin Actions Available:
- Monitor payment completion
- View dispute history
- Next State:
Released(when seller releases)
5. Released
- Meaning: The seller has released the funds, completing the dispute resolution process.
- Admin Actions Available:
- View dispute history (dispute is closed)
- Final State: No further actions possible
State Transition Flow
stateDiagram-v2
[*] --> Initiated: Dispute Created
Initiated --> InProgress: Admin Takes Dispute
InProgress --> SellerRefunded: Resolve for Seller
InProgress --> Settled: Resolve for Buyer
Settled --> Released: Seller Releases
SellerRefunded --> [*]: Dispute Closed
Released --> [*]: Dispute Closed
State Color Coding
In the UI, dispute states are color-coded for quick visual identification:
- Yellow:
Initiated(pending/waiting) - Green:
InProgress,Settled,Released(active/resolved) - Red:
SellerRefunded(refunded/canceled)
Source: src/ui/mod.rs:482 (apply_status_color)
Dispute Resolution Flow
Taking a Dispute
When an admin takes a dispute from the disputes list:
sequenceDiagram
participant Admin
participant TUI
participant Client
participant DB
participant AdminKey
participant NostrRelays
participant Mostro
Admin->>TUI: Navigate to Disputes tab
TUI->>Client: Display dispute list
Admin->>TUI: Select dispute & press Enter
TUI->>Client: take_dispute(dispute_id)
Client->>DB: Get admin_privkey
DB-->>Client: admin_privkey
Client->>AdminKey: Parse & sign with admin key
Client->>Client: Construct TakeDispute message
Client->>NostrRelays: Publish NIP-59 Gift Wrap
NostrRelays->>Mostro: Forward TakeDispute action
Mostro->>Mostro: Validate admin key & assign dispute
Mostro->>Mostro: Update dispute status: Initiated β InProgress
Mostro->>NostrRelays: Confirmation
NostrRelays-->>Client: Response
Client->>Client: Update dispute status to InProgress
Client-->>TUI: Dispute taken successfully
TUI-->>Admin: Show confirmation
Key Points:
- Only the
admin_privkeycan sign dispute resolution actions - The dispute is assigned to the admin who takes it
- Other admins cannot take a dispute that's already been taken
- The admin becomes responsible for resolving the dispute
- Upon taking a dispute, the admin receives a
SolverDisputeInfostruct with all dispute details
Dispute Information Structure
When an admin takes a dispute, they receive a SolverDisputeInfo struct containing all relevant information about the dispute:
pub struct SolverDisputeInfo {
pub id: Uuid,
pub kind: String,
pub status: String,
pub hash: Option<String>,
pub preimage: Option<String>,
pub order_previous_status: String,
pub initiator_pubkey: String,
pub buyer_pubkey: Option<String>,
pub seller_pubkey: Option<String>,
pub initiator_full_privacy: bool,
pub counterpart_full_privacy: bool,
pub initiator_info: Option<UserInfo>,
pub counterpart_info: Option<UserInfo>,
pub premium: i64,
pub payment_method: String,
pub amount: i64,
pub fiat_amount: i64,
pub fee: i64,
pub routing_fee: i64,
pub buyer_invoice: Option<String>,
pub invoice_held_at: i64,
pub taken_at: i64,
pub created_at: i64,
}
Field Descriptions
Identity & Status:
id: Unique identifier (UUID) for the order associated with this dispute. Mostrix stores this as the primary key in theadmin_disputestable and uses it as the ID sent to Mostro when performing admin finalization actions (AdminSettle/AdminCancel). Optionalbond_resolutionpayload is sent viaBondSlashChoice::to_optional_payload()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 section)order_previous_status: The order's status before the dispute was initiated
Lightning Network Details:
hash: Lightning invoice hash (if applicable)preimage: Lightning invoice preimage (if available)buyer_invoice: Lightning invoice provided by the buyer (if applicable)invoice_held_at: Timestamp when the invoice was held/created
Parties Involved:
initiator_pubkey: Public key of the user who initiated the disputebuyer_pubkey: Public key of the buyer (if available)seller_pubkey: Public key of the seller (if available)initiator_full_privacy: Whether the dispute initiator has full privacy enabledcounterpart_full_privacy: Whether the counterparty has full privacy enabledinitiator_info: Optional user information for the dispute initiator (name, reputation, etc.)counterpart_info: Optional user information for the counterparty (name, reputation, etc.)
Financial Details:
amount: Amount in satoshisfiat_amount: Amount in fiat currencypremium: Premium amount (in satoshis)fee: Fee amount (in satoshis)routing_fee: Lightning routing fee (in satoshis)payment_method: Payment method used
Timestamps:
created_at: Timestamp when the dispute was createdtaken_at: Timestamp when the admin took the dispute
Using Dispute Information
This comprehensive information allows admins to:
- Understand the context: Review order details, parties involved, and dispute circumstances
- Assess privacy settings: Know if parties have full privacy enabled (affects available information)
- Review financial details: Understand amounts, fees, and payment methods
- Check Lightning status: Verify invoice details and payment state
- Make informed decisions: Use all available information to resolve the dispute fairly
Privacy Considerations:
- If
initiator_full_privacyorcounterpart_full_privacyistrue, some user information may be limited initiator_infoandcounterpart_infomay beNoneif privacy is enabled- Admins should respect privacy settings while gathering necessary information for resolution
Data Validation:
- Required Fields:
buyer_pubkeyandseller_pubkeyare validated when taking a dispute. If either field is missing, the dispute cannot be saved to the database and an error is displayed. - Data Integrity: The finalization popup also validates these fields before displaying dispute details. If data is incomplete, a "Data Integrity Error" popup is shown instead of the finalization options.
- Shared-key sanity: When saving a dispute, the client checks that buyer and seller derived shared keys differ when the two pubkeys differ. If they are identical, an error is logged (see KEY_MANAGEMENT.md).
Post-Finalization Action Blocking:
Once a dispute is finalized (status: Settled, SellerRefunded, or Released), the AdminSettle and AdminCancel actions are blocked at multiple levels:
- Model Layer:
AdminDispute::is_finalized()returnstruefor finalized disputes. The helper methodscan_settle()andcan_cancel()returnfalsewhen finalized. - UI Layer: The finalization popup disables and grays out the pay/refund buttons (inner body
β). - Handler Layer:
execute_finalize_dispute()checks the dispute state before executing any action. If the dispute is already finalized, it returns an error: "Cannot execute [action]: dispute is already finalized". - Key Handler Layer: When pressing Enter on a disabled action button, an error message is displayed: "Cannot finalize: dispute is already finalized".
This multi-layered protection ensures that finalized disputes cannot be accidentally or maliciously modified.
Source: src/models.rs (AdminDispute::is_finalized, can_settle, can_cancel), src/util/order_utils/execute_finalize_dispute.rs
Adding a Solver
Status: β Implemented and Working
When an admin adds another dispute solver from the Settings tab:
sequenceDiagram
participant Admin
participant TUI
participant Client
participant Validation
participant AdminKey
participant NostrRelays
participant Mostro
participant NewSolver
Admin->>TUI: Navigate to Settings tab
Admin->>TUI: Select "Add Dispute Solver"
TUI->>TUI: Show input popup
Admin->>TUI: Enter solver public key (npub... or hex)
Admin->>TUI: Press Enter
TUI->>Validation: Validate npub/hex format
alt Invalid Format
Validation-->>TUI: Error: "Invalid key format"
TUI-->>Admin: Show error popup
else Valid Format
TUI->>TUI: Show confirmation popup
Admin->>TUI: Confirm (Enter on YES)
TUI->>Client: execute_admin_add_solver(solver_pubkey, admin_keys)
Client->>AdminKey: Use live runtime admin key
Client->>Client: Construct AdminAddSolver message
Client->>NostrRelays: Send NIP-59 Gift Wrap DM
NostrRelays->>Mostro: Forward AdminAddSolver action
Mostro->>Mostro: Validate & add solver
Mostro->>NostrRelays: admin-add-solver response
NostrRelays-->>Client: Response DM
Client->>Client: Validate sender + request_id + action
Client-->>TUI: Success/Error result
TUI-->>Admin: Show result popup
TUI->>TUI: Stay on Settings tab
Note over Mostro,NewSolver: New solver can now<br/>resolve disputes
end
Implementation Notes:
execute_admin_add_solvernow uses live runtime admin keys passed by the UI context, not startupSETTINGSsnapshots, so admin key changes apply immediately without restart.- The Add Solver flow now waits for Mostro response (
wait_for_dm+parse_dm_events) and validates:- response sender is
mostro_pubkey - request id (
handle_mostro_response) - response action is
AdminAddSolver
- response sender is
- UI shows a dedicated waiting popup while the command is in-flight.
- Validation accepts both bech32 and hex key formats for Settings input:
validate_npub:npub1...or 64-char hex public keyvalidate_nsec:nsec1...or 64-char hex secret key
Key Points:
- β
Requires admin privileges (signed with
admin_privkey) - β
Input validation: accepts
npub1...and 64-char hex public keys - β Error handling: shows error popup for invalid key format or missing/invalid Mostro response
- β Confirmation popup: Custom message "Are you sure you want to add this pubkey as dispute solver?"
- β UI state management: stays on Settings tab and shows a waiting popup while request is pending
- β Adds a new public key to the list of authorized dispute solvers
- β The new solver can then take and resolve disputes
- β Helps distribute dispute resolution workload
- β Uses NIP-59 Gift Wrap for secure message delivery
Chatting with Parties
Status: β Fully Implemented with Real-time UI
Admins communicate with buyers and sellers through an integrated chat interface within the "Disputes in Progress" tab. The chat system provides a rich, interactive experience with real-time visual feedback.
Chat Features
Visual Design:
- Dynamic input box: Grows from 1 to 10 lines based on message length
- Focus indicators: Bold yellow border when typing, gray when inactive
- Party switching: Use Tab key to switch between Buyer and Seller chat views
Message Management:
- Per-dispute storage: Each dispute maintains its own chat history
- 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
- Buyer messages: Only shown when viewing the Buyer chat
- Seller messages: Only shown when viewing the Seller chat
- Scroll support:
- PageUp/PageDown: Navigate through message history
- End: Jump to bottom of chat (latest messages)
- Visual scrollbar: Right-side scrollbar shows position in chat history (β/β/β/β symbols)
- Auto-scroll: Automatically scrolls to newest messages after sending
- Persistent history: All messages stored in
admin_dispute_chatsHashMap
Input Handling:
- Direct typing: Start typing to add text to input (when input is enabled)
- Input toggle: Press Shift+I to enable/disable chat input
- When disabled, prevents accidental typing while navigating
- Visual indicator shows "disabled - Shift+I to enable" in input title
- Input is enabled by default when entering dispute management
- Text wrapping: Input wraps at word boundaries with trim behavior
- Multi-line support: Supports up to 10 lines with visual growth
- Send on Enter: Press Enter to send message (or finalize if input is empty)
- Clear after send: Input automatically clears after sending
Chat with Buyer Flow
sequenceDiagram
participant Admin
participant TUI
participant ChatState
participant Client
participant AdminKey
participant NostrRelays
participant Buyer
Admin->>TUI: Select dispute in sidebar
TUI->>TUI: Show buyer chat (Tab switches to buyer)
Admin->>TUI: Type message (direct input)
TUI->>ChatState: Update admin_chat_input
TUI->>TUI: Dynamic input box grows
Admin->>TUI: Press Enter
TUI->>ChatState: Create DisputeChatMessage (Admin sender)
ChatState->>ChatState: Store in admin_dispute_chats[dispute_id]
TUI->>Client: send_message_to_buyer(message)
Client->>AdminKey: Sign with admin key
Client->>NostrRelays: Publish encrypted DM (NIP-59)
NostrRelays->>Buyer: Forward message
Buyer->>NostrRelays: Send response
NostrRelays->>Client: Receive response
Client->>ChatState: Create DisputeChatMessage (Buyer sender)
ChatState->>TUI: Update UI with new message
TUI->>Admin: Display buyer response in green
Chat with Seller Flow
sequenceDiagram
participant Admin
participant TUI
participant ChatState
participant Client
participant AdminKey
participant NostrRelays
participant Seller
Admin->>TUI: Select dispute in sidebar
TUI->>TUI: Show seller chat (Tab switches to seller)
Admin->>TUI: Type message (direct input)
TUI->>ChatState: Update admin_chat_input
TUI->>TUI: Dynamic input box grows
Admin->>TUI: Press Enter
TUI->>ChatState: Create DisputeChatMessage (Admin sender)
ChatState->>ChatState: Store in admin_dispute_chats[dispute_id]
TUI->>Client: send_message_to_seller(message)
Client->>AdminKey: Sign with admin key
Client->>NostrRelays: Publish encrypted DM (NIP-59)
NostrRelays->>Seller: Forward message
Seller->>NostrRelays: Send response
NostrRelays->>Client: Receive response
Client->>ChatState: Create DisputeChatMessage (Seller sender)
ChatState->>TUI: Update UI with new message
TUI->>Admin: Display seller response in red
Chat Data Structures
Source: src/ui/mod.rs
/// Represents the sender of a chat message
pub enum ChatSender {
Admin,
Buyer,
Seller,
}
/// A chat message in the dispute resolution interface
pub struct DisputeChatMessage {
pub sender: ChatSender,
pub content: String,
pub timestamp: i64, // Unix timestamp
pub target_party: Option<ChatParty>, // For Admin messages: which party this was sent to
}
/// Per-(dispute, party) last-seen timestamp for admin chat
pub struct AdminChatLastSeen {
pub last_seen_timestamp: Option<u64>, // Last seen message timestamp for incremental fetches
}
// Stored in AppState
pub admin_dispute_chats: HashMap<String, Vec<DisputeChatMessage>>,
pub admin_chat_scrollview_state: tui_scrollview::ScrollViewState,
pub admin_chat_selected_message_idx: Option<usize>,
pub admin_chat_line_starts: Vec<usize>,
pub admin_chat_scroll_tracker: Option<(String, ChatParty, usize)>,
pub admin_chat_last_seen: HashMap<(String, ChatParty), AdminChatLastSeen>,
Receiving and saving file attachments
Buyers and sellers can send encrypted file or image attachments in dispute chat. The format follows Mostro Mobile Encrypted File Messaging: JSON messages with type image_encrypted or file_encrypted, containing blossom_url, nonce (hex-encoded 12 bytes), filename, original_size, encrypted_size, optional mime_type, and for images width and height (required by the mobile client). Optional key (base64, 32 bytes) when not using shared-key decrypt.
- Display: Attachment messages appear in the chat with an icon (πΌ Image or π File), filename, and "(key provided)" when the sender included a decryption key. The chat block title shows a file count when non-zero (e.g. "Chat with Buyer (12 messages, 2 file(s))"). A transient yellow toast notifies when a new attachment is received; it clears after 8 seconds or on any key press.
- Persistence: Transcript files under
~/.mostrix/disputes_chat/<dispute_id>.txtstore attachment metadata as JSON (sameimage_encrypted/file_encryptedshape as on the wire) viaserialize_attachment_for_transcript; file bytes are not stored until the admin saves with Ctrl+S. On restart,load_chat_from_filerestoresChatAttachmentso the save popup and file count work without waiting for relay. Older files with[Image: name - Ctrl+S to save]placeholders are hydrated whenapply_admin_chat_updatesreceives the same attachment from relay at the matching timestamp. - Save (Ctrl+S):
- From the dispute chat, press Ctrl+S to open a Save attachment popup. The popup lists all file/image attachments in the current dispute for the active party (Buyer or Seller). If there are no attachments, Ctrl+S does nothing.
- In the popup: Use β/β to select an attachment, Enter to save the selected one, Esc to cancel. The popup shows one line per attachment (πΌ image or π file + filename) and a footer hint: "ββ Select, Enter Save, Esc Cancel".
- Saving downloads the file from the Blossom URL (resolved from
blossom://tohttps://), optionally decrypts with ChaCha20-Poly1305 when the sender provided a key (or when the admin can derive the shared key from the partyβs pubkey), and writes to~/.mostrix/downloads/<dispute_id>_<sanitized_filename>(or_<filename>.encif no key). The downloads directory is created if needed. Success or error is shown in the shared operation result popup when the background download finishes;main.rsdrainssave_attachment_rx/order_result_rxbefore each frame so the modal does not require an extra keypress (see STARTUP_AND_CONFIG.md). When a party later provides the corresponding shared key, the admin can use the Observer tab to fetch and view the full chat from relays, including any attachments.
- Cipher: Blob layout is nonce (12 bytes) + ciphertext + authentication tag (16 bytes); decryption uses the
chacha20poly1305crate. Max blob size is 25 MB per download. Blossom HTTP: save uses unauthenticated GET by URL (fetch_blob); My Trades outbound upload (Ctrl+O) uses NIP-24242 PUT auth signed with the order trade key β see MESSAGE_FLOW_AND_PROTOCOL.md β "Attachments (send)".
Source: src/util/blossom.rs (URL resolution, fetch, decrypt, save), src/ui/helpers/attachments.rs (parse, serialize, legacy placeholder match), src/ui/helpers/chat_storage.rs (JSON transcript save/load), src/ui/helpers/chat_render.rs (chat list/line styling).
NIP-59 Chat Flow (Admin β Parties β Shared Key Model)
-
Shared key derivation:
- When a dispute is taken (
AdminDispute::new), per-party shared keys are eagerly derived using ECDH:nostr_sdk::util::generate_shared_key(admin_secret, counterparty_pubkey). - Two shared keys are stored (as hex) in the
admin_disputestable:buyer_shared_key_hexandseller_shared_key_hex. - The same derivation is used by
mostro-chatso both the admin and the counterparty can independently derive the same shared key.
- When a dispute is taken (
-
Message addressing:
- Admin chat messages are addressed to the shared key's public key (not the counterparty's trade pubkey directly).
- The admin reads
admin_privkeyfromsettings.tomlto sign the inner rumor; the gift wrapptag targets the shared key pubkey. - Per-party timestamps are tracked in
AppState.admin_chat_last_seenunder(dispute_id, ChatParty).
-
Sending messages:
- Admin chat messages are wrapped into NIPβ59
GiftWrapevents addressed to the shared key's public key:- Rumor content: Mostro protocol format
(Message::Dm(SendDm, TextMessage(...)), None). - The gift wrap is built using
EventBuilder::gift_wrapwith the admin keys and the shared key pubkey as recipient.
- Rumor content: Mostro protocol format
- The event is then published to the relays.
- Admin chat messages are wrapped into NIPβ59
-
Receiving messages:
- A background task periodically polls for new
GiftWrapevents addressed to each shared key's public key:- Rebuilds
Keysfrom the storedbuyer_shared_key_hex/seller_shared_key_hex. - Uses
last_seen_timestampto only process messages created after the last processed one. - Decrypts each event using the shared key (standard NIP-59 or simplified mostro-chat format).
- Skips messages signed by the admin identity (already added locally on send).
- Rebuilds
- A background task periodically polls for new
-
Behavior on restart (Chat Restore at Startup):
- Admin chat has full restart-safe behavior:
-
Chat messages are persisted as human-readable transcripts under:
~/.mostrix/disputes_chat/<dispute_id>.txt -
At startup,
recover_admin_chat_from_files:- Reads each transcript file.
- Rebuilds
admin_dispute_chatsso existing disputes immediately show their chat history in the UI. - Computes perβparty max timestamps and updates
AppState.admin_chat_last_seen.
-
These timestamps are also stored in the
admin_disputestable asbuyer_chat_last_seenandseller_chat_last_seen. -
The shared-key chat subscription router (
listen_for_chat_messagesinsrc/util/chat_listener.rs) uses these DB fields as cursors to hydrate history once per key on track (fetch_gift_wraps_for_shared_key), then receives newer NIPβ59 events live over one batchedkind: 1059subscription. Disputes are tracked viatrack_dispute_chatwhen taken and re-tracked bytrack_startup_chatsat startup/reconnect.
-
- This hybrid approach keeps the protocol stateless while giving admins a smooth, restart-safe chat experience across application restarts.
- Admin chat has full restart-safe behavior:
Keyboard Shortcuts
In Chat Interface:
- Type: Start typing message directly (when input enabled)
- Enter: Send message (when input has text)
- Shift+F: Open finalization popup for the currently selected dispute
- Tab: Switch between Buyer and Seller chat views
- PageUp/PageDown: Scroll through message history
- End: Jump to bottom of chat (latest messages)
- Shift+I: Toggle chat input enabled/disabled
- Ctrl+S: Open save-attachment popup (when the current dispute/party has attachments). In the popup: β/β select attachment, Enter save selected to disk, Esc cancel.
- Backspace: Delete characters from input (when input enabled)
- Up/Down: Select different dispute in sidebar
Visual Safety Features:
- Color differentiation: Buyer (Green) and Seller (Magenta) messages clearly distinguished
- Message headers: Each message displays "Sender - date - time" format with color-coded sender names (Cyan for Admin, Green for Buyer, Magenta for Seller)
- Clear party label: "Chat with Buyer" or "Chat with Seller" in chat header
- Dynamic footer: Shows different shortcuts based on input focus and enabled state; shows "Ctrl+S: Save file" when the current dispute/party has attachments (opens the save-attachment list popup)
- Privacy icons: π’ (info available) or π΄ (private) for each party
- Context preservation: Each dispute maintains its own complete message history
- Visual scrollbar: Right-side scrollbar (β/β/β/β) indicates scroll position in chat
- Input state indicators: Clear visual feedback when input is enabled/disabled
Implementation Details
Message Storage:
- Messages stored in
HashMap<String, Vec<DisputeChatMessage>>keyed by dispute ID - Each message includes sender, content, and timestamp
- History persists for the lifetime of the application session
Text Wrapping Algorithm:
- Calculates available width (terminal width - borders - padding)
- Simulates ratatui's wrap behavior with trim: true
- Finds word boundaries for natural wrapping
- Hard breaks at available width when no spaces found
- Caps at 10 lines maximum with visual indicators
Performance Optimizations:
- Pubkey-to-dispute routing uses
HashMap<PublicKey, (String, ChatParty)>for O(1) lookups. - Chat message sending is spawned as an async task (
tokio::spawn) to avoid blocking the UI thread. - Gift wrap fetching uses a 7-day rolling window to limit relay queries.
- Unified
update_chat_last_seen_by_dispute_idfunction replaces separate buyer/seller update methods.
Source Files:
src/ui/disputes_in_progress_tab.rs- Chat UI rendering, dynamic input sizing, scrollbar, attachment toast, block title file count, footer hintsrc/ui/key_handler/input_helpers.rs- Non-blocking message sending viatokio::spawnsrc/ui/key_handler/mod.rs- Chat input handling (prioritized over other inputs), Shift+I toggle, End key, Ctrl+S open save-attachment popup and popup key handling (Up/Down/Enter/Esc)src/ui/save_attachment_popup.rs- Save attachment popup rendering (centered list, selection highlight, footer hint)src/ui/helpers/mod.rs- Compatibility re-export layer for helper APIs used across UI modulessrc/ui/helpers/startup.rs- Startup hydration, admin/user chat update application, and last-seen cursor updatessrc/ui/helpers/chat_storage.rs- Chat transcript parsing/loading/saving and idempotent append logicsrc/ui/helpers/attachments.rs- Attachment parsing, placeholder text, and attachment toast helperssrc/ui/helpers/chat_render.rs/src/ui/helpers/chat_visibility.rs- Chat list/scrollview rendering and party visibility filteringsrc/util/chat_utils.rs- NIP-59 gift wrap fetch/send, HashMap-based message routingsrc/util/blossom.rs- Blossom URL resolution, blob fetch, ChaCha20-Poly1305 decryption, save to~/.mostrix/downloads/src/models.rs- Unifiedupdate_chat_last_seen_by_dispute_idfor DB persistence
Dispute Resolution Actions
Once an admin has taken a dispute (state: InProgress), they are expected to perform resolution actions such as resolving in favor of buyer or seller, requesting additional information, or transferring/escalating the dispute. The exact UI flows for these actions are still under active development in Mostrix and may change; refer to the Mostro protocol documentation for the canonical dispute actions and state transitions.
Security Considerations
Admin Key Management
admin_privkey: Must be kept secure and never shared- Key derivation: Admin keys are not derived from the user's mnemonic (separate key)
- Access control: Only the configured admin key can sign dispute actions
Dispute Assignment
- Single admin per dispute: Once taken, a dispute is assigned to one admin
- Prevents conflicts: Other admins cannot take an already-assigned dispute
- Clear ownership: The assigned admin is responsible for resolution
Communication Security
- Encrypted messages: All communication uses NIP-44 or NIP-59 encryption
- Signed actions: All dispute actions are signed with the admin key
- Audit trail: Dispute actions are recorded on the Nostr network
New Features: Currency Filters & Relay Management
Currency Filter Management
Status: β Implemented (as a view filter)
Admins (and users) can configure local currency filters to focus on specific fiat currencies when viewing orders. The set of available currencies still comes from the Mostro instance status eventβs fiat_currencies_accepted tag (Mostro Instance Status), but the currencies_filter field in settings.toml is used as a filter over those orders.
Currency Filter Features
- Add Currency Filter: Adds fiat currency codes (e.g., USD, EUR, ARS) into the
currencies_filterarray insettings.toml.- Codes are validated (non-empty, max 10 characters) and uppercased.
- When non-empty, background order fetching only keeps orders whose
fiat_codeis in this list.
- Clear Currency Filters: Removes all currency filters
- Clears the
currencies_filterarray insettings.toml - An empty list means βno filterβ: all currencies from the Mostro instance are shown.
- Clears the
- Dynamic Filtering: Currency filters are applied on each periodic fetch; changes in
settings.tomltake effect without restarting. - Status Bar Display: The status barβs currency line still shows currencies coming from the Mostro instance status event; filters only control which orders are visible, not which currencies the instance supports.
Currency Filter Implementation
Source: src/ui/key_handler/settings.rs:55-78
/// Save currency to settings file
pub fn save_currency_to_settings(currency_string: &str) {
save_settings_with(
|s| {
let currency_upper = currency_string.trim().to_uppercase();
if !s.currencies_filter.contains(¤cy_upper) {
s.currencies_filter.push(currency_upper);
}
},
"Failed to save currency to settings",
"Currency filter added to settings file",
);
}
/// Clear all currency filters (sets currencies to empty vector)
pub fn clear_currency_filters() {
save_settings_with(
|s| {
s.currencies_filter.clear();
},
"Failed to clear currency filters",
"All currency filters cleared",
);
}
Relay Management Improvements
Status: β Fully Implemented
Enhanced relay management allows admins to dynamically add relays without restarting the application.
Features
- Dynamic Relay Addition: New relays are added to the running Nostr client immediately
- No restart required
- Relays are connected asynchronously in the background
- Settings Persistence: Relays are saved to
settings.tomland persist across restarts - Duplicate Prevention: The system prevents adding the same relay twice
- Status Bar Display: Active relays are displayed in the status bar
Implementation
Source: src/ui/key_handler/enter_handlers.rs (relay addition logic)
When a relay is added:
- Input is validated (must start with
wss://) - Confirmation popup is shown
- Relay is added to
settings.toml - Relay is added to the running Nostr client via
tokio::spawn - Success/error feedback is provided
Validation Enhancements
Status: β Fully Implemented
Comprehensive input validation ensures data integrity and provides clear error messages.
Validation Rules
-
Mostro Pubkey Validation (
validate_mostro_pubkey)- Format: 64-character hex string
- Changed from
npubformat to hex format for consistency - Example:
627788f4ea6c308b98e5928a632e8220108fcbb7fbcc1270e67582d98eac84ae
-
Relay Validation (
validate_relay)- Must start with
wss://orws:// - Prevents invalid relay URLs
- Must start with
-
Currency Validation (
validate_currency)- Non-empty string
- Maximum 10 characters
- Automatically converted to uppercase
-
Nostr Public Key Validation (
validate_npub)- Must be valid
npubformat (bech32 encoded) - Used for admin keys and dispute solver pubkeys
- Must be valid
Source: src/ui/key_handler/validation.rs
Status Bar Improvements
Status: β Fully Implemented
The status bar now provides comprehensive information about current settings and configuration.
Multi-line Display
The status bar displays 3 separate lines:
- Mostro Name + Pubkey: Shows the current Mostro instance alias (Lightning node alias) and pubkey
- Relays List: Shows all active relays (comma-separated, truncated if many)
- Currencies List: Shows active currency filters (comma-separated, or "All" if none)
Dynamic Updates
- Status bar reloads settings from disk on each draw cycle
- Ensures the displayed information is always current
- Updates immediately when settings are changed
Source: src/ui/status.rs, src/ui/mod.rs (status bar rendering)
Implementation Status
β Completed Features (Current PR)
The following features have been fully implemented and are working:
Settings Tab Improvements
-
Settings Tab for Both Modes (
src/ui/settings_tab.rs)- β User and Admin mode support with role-specific options
- β Mode display showing current role
- β
Mode switching via
Mkey with footer instructions - β Dynamic option list based on user role
-
Common Settings Functions (Available to both User and Admin)
- β
Change Mostro Pubkey (
src/ui/key_handler/settings.rs:29-36)- Input popup with keyboard support
- Hex format validation (64 characters)
- Confirmation popup before saving
- Persists to
settings.toml
- β
Add Nostr Relay (
src/ui/key_handler/settings.rs:38-49)- Input popup with keyboard support
- Validation (must start with
wss://) - Confirmation popup before saving
- Adds to relay list in
settings.toml - Prevents duplicate relays
- Dynamically adds relay to running Nostr client
- β
Add Currency Filter (
src/ui/key_handler/settings.rs:55-67)- Input popup with keyboard support
- Currency code validation (non-empty, max 10 chars)
- Automatically converts to uppercase
- Prevents duplicate currencies
- Applied in real-time to order filtering
- Persists to
settings.toml
- β
Clear Currency Filters (
src/ui/key_handler/settings.rs:69-78)- Confirmation popup before clearing
- Clears all currency filters
- Updates
settings.tomlimmediately - Applied in real-time to order filtering
- β
Change Mostro Pubkey (
-
Admin-Only Settings Functions
- β
Add Dispute Solver (
src/util/order_utils/execute_admin_add_solver.rs)- Input validation for npub format
- Error popup for invalid input
- Confirmation popup with custom message
- Sends
AdminAddSolveraction to Mostro - Stays on Settings tab after completion
- β
Change Admin Key (
src/ui/key_handler/settings.rs:20-27)- Input popup with keyboard support
- Confirmation popup before saving
- Persists to
settings.toml
- β
Add Dispute Solver (
Code Quality Improvements
-
Modular Key Handler (
src/ui/key_handler/)- β
Refactored monolithic
key_handler.rsinto modular structure - β
mod.rs: Main dispatcher - β
input_helpers.rs: Generic text input handling - β
navigation.rs: Navigation and tab switching - β
enter_handlers.rs: Enter key handling with validation - β
esc_handlers.rs: Escape key handling - β
form_input.rs: Character input and backspace - β
confirmation.rs: Confirmation popup logic - β
settings.rs: Settings persistence functions - β
validation.rs: Input validation utilities
- β
Refactored monolithic
-
UI Components
- β
Generic key input popup (
src/ui/key_input_popup.rs) - β
Enhanced confirmation popup (
src/ui/admin_key_confirm.rs)- Supports custom messages
- Conditionally hides key display
- Proper text formatting
- β
Generic key input popup (
-
Validation System (
src/ui/key_handler/validation.rs)- β
validate_mostro_pubkey: Hex format validation (64 characters) - β
validate_relay: URL format validation (wss://orws://) - β
validate_currency: Currency code validation (non-empty, max 10 chars) - β
validate_npub: Nostr public key validation (bech32 format) - β
All validation functions return
Result<(), String>with clear error messages
- β
-
Status Bar Enhancements (
src/ui/status.rs)- β Multi-line display (Mostro pubkey, relays, currencies)
- β Dynamic settings reloading from disk
- β Real-time updates when settings change
- β Truncation for long lists
Commits Made
The following commits were made in this PR:
-
0aee5fa-refactor: split key_handler into modular structure and improve settings UX- Refactored key handler into modular structure
- Added settings functions for Mostro pubkey and relay
- Improved code organization and reusability
-
afd7ed9-feat: add admin key confirmation popup with settings persistence- Added admin key confirmation popup
- Implemented settings persistence
-
c42fab8-fix:(latest commit)- Fixed footer instruction display for Admin mode
- Ensured proper layout for Settings tab
π Planned Implementation
- Enhanced Chat tab for admins (separate buyer/seller views with color coding).
- Additional dispute resolution actions and workflows in the UI.
Source: src/ui/mod.rs:113
pub enum AdminTab {
Disputes,
Chat,
Settings,
}
Note: The Chat tab is currently rendered as βcoming soonβ in the UI. Buyer/sellerβspecific chat tabs described above are design goals and not yet implemented.
Testing
Mostrix includes a comprehensive test suite to ensure reliability and correctness of critical components.
Test Organization
Tests are organized into two categories:
-
Unit Tests (inline in source files): Test pure functions and isolated logic
- Parsing functions (
src/util/order_utils/helper.rs) - Validation functions (
src/ui/key_handler/validation.rs) - Helper functions (
src/util/types.rs)
- Parsing functions (
-
Integration Tests (
tests/directory): Test database operations and end-to-end flows- Database operations (
tests/db_tests.rs) - Shared test utilities (
tests/common/mod.rs)
- Database operations (
Running Tests
# Run all tests
cargo test
# Run only unit tests (faster)
cargo test --lib
# Run only integration tests
cargo test --test db_tests
# Run with output
cargo test -- --nocapture
Test Coverage
The test suite covers:
- Parsing Logic: Order and dispute parsing from Nostr tags
- Validation: Public key validation, range amount validation
- Database Operations: User creation, key derivation, order persistence
- Key Derivation: Critical security component - ensures deterministic key generation
- Error Handling: Error message generation and validation
Key Derivation Tests
Key derivation is a critical security component and is thoroughly tested:
- Same mnemonic + index produces same keys (deterministic)
- Different indices produce different keys
- Identity keys are correctly derived from mnemonic
Source: src/models.rs (inline tests), tests/db_tests.rs
Future Test Expansion
The test infrastructure is designed for easy expansion. Prefer ratatui::backend::TestBackend for deterministic TUI render asserts (see CODING_STANDARDS.md and src/ui/helpers/layout.rs). Further additions could include:
- Mock-based tests for async operations (Nostr client interactions)
- Broader UI state-transition coverage (key handler waves in
.cursor/rules/test-coverage.mdc) - Snapshot testing for complex data structures
- End-to-end workflow tests
Related Documentation
- TUI_INTERFACE.md - General TUI architecture and navigation
- KEY_MANAGEMENT.md - Key derivation and management
- MESSAGE_FLOW_AND_PROTOCOL.md - Message protocols and flows
- CODING_STANDARDS.md - Code quality guidelines including testing practices
- FEATURE_ANALYSIS.md - Comprehensive analysis of currency filters and relay management features