This guide explains Mostrix’s boot sequence and configuration surfaces.
- Entry:
src/main.rs:98 - Initializes database, derives identity keys, initializes settings, then logger, terminal (raw mode), shared state, Nostr client, and background tasks.
- Shows an animated startup splash (Mostro wordmark + loading dots) while post-terminal init runs; see Startup splash below.
- Enters the main event loop to handle UI updates and user input.
After the terminal enters alternate screen mode, Mostrix draws a full-screen splash until background boot work finishes (src/startup.rs, src/ui/startup_splash.rs).
- Wordmark: multi-line logo from the project art (same style as the desktop
logo.txtexport). - Loading dots: reusable glyph
<>repeated 1–4 times on the last logo row, each prefixed by a space, cycled every ~400 ms while the splash tick runs (~150 ms redraw interval). - Phase text: short status under the art (
Starting…,Connecting to relays…,Loading market data…,Restoring chats…,Almost ready…) updated as init steps complete. - Minimum display: splash stays visible for at least ~800 ms so fast boots do not flash the screen.
- Narrow terminals: if the terminal is narrower than the padded logo width, a one-line
mostro is loading+ dots + phase is shown instead. - Background: full-screen fill via
fill_splash_background(solidBACKGROUND_COLORblock, same pattern as the Exit tab) so ASCII art and status text do not show mismatched per-span backgrounds. - CI / scripts: set
MOSTRIX_NO_SPLASH=1to skip the splash loop and run init directly.
The database is initialized at startup to ensure the schema is ready.
Source: src/db.rs
- Creates the SQLite database file at
~/.mostrix/mostrix.db. - Ensures tables exist (
orders,users). - If the
userstable is empty,User::new()generates a new 12-word BIP-39 mnemonic and persists it in theuserstable (this mnemonic is the root for user identity/trade key derivation). - For existing databases, runs migrations automatically to keep the schema up to date.
Mostrix uses centralized settings management in src/settings.rs.
Source: src/settings.rs
pub fn init_settings(identity_keys: Option<Keys>)
-> Result<InitSettingsResult, anyhow::Error>- On first run,
settings.tomlis generated from an embedded template compiled into the binary (rather than copying from the repo root). - If
identity_keysis provided (derived from the DB identity/index-0 key), Mostrix derives thensec_privkeyforsettings.tomlso DB keys and settings keys match. - The returned
InitSettingsResult.did_generate_new_settings_fileindicates whether this process generated a brand-newsettings.toml. - When
did_generate_new_settings_fileistrue,main.rsshows theBackupNewKeyspopup overlay immediately on the current initial tab, prompting the user to save the generated 12-word mnemonic.
Error Handling: Startup failures in init_settings() are propagated as anyhow::Error (causing a clean process exit with an error message). If settings are accessed later at runtime before initialization (via the SETTINGS global), those failures are surfaced as user-friendly messages using OperationResult::Error instead of panicking. This ensures graceful degradation and clear feedback to users in both cases.
Logging is configured via setup_logger in src/main.rs.
Source: src/main.rs:41
fn setup_logger(level: &str) -> Result<(), fern::InitError> {
let log_level = match level.to_lowercase().as_str() {
// ... level mapping ...
};
Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{}] [{}] - {}",
Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
message
))
})
.level(log_level)
.chain(fern::log_file("app.log")?) // Writes to app.log
.apply()?;
Ok(())
}
- Sets the log level based on the
log_levelfield insettings.toml. - Outputs log messages to
app.log.
The TUI uses ratatui with the crossterm backend.
Source: src/main.rs:104
enable_raw_mode()?;
let mut out = stdout();
execute!(
out,
EnterAlternateScreen,
crossterm::event::EnableMouseCapture
)?;
let backend = CrosstermBackend::new(out);
let mut terminal = Terminal::new(backend)?;
- Enables terminal raw mode.
- Enters the alternate screen and enables mouse capture.
The Settings struct defines all available configuration options.
Source: src/settings.rs
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Settings {
pub mostro_pubkey: String,
pub nsec_privkey: String,
pub admin_privkey: String,
pub relays: Vec<String>,
pub log_level: String,
pub currencies_filter: Vec<String>,
#[serde(default = "default_user_mode")]
pub user_mode: String, // "user" or "admin", default "user"
#[serde(default)]
pub ln_address: String, // Lightning address for buyer receive; empty = unset
#[serde(default)]
pub blossom_servers: Vec<String>, // Blossom upload hosts; empty = built-in defaults
}mostro_pubkey: The public key of the Mostro instance to interact with.nsec_privkey: The user's Nostr private key (nsec format).admin_privkey: The admin's private key, required for solving disputes when in admin mode.relays: A list of Nostr relay URLs to connect to.log_level: The verbosity of logging (e.g., "debug", "info", "warn", "error").currencies_filter: Optional list of fiat currency filters (ISO codes).- When empty, all currencies published by the Mostro instance are shown.
- When non-empty (e.g.
["USD"],["USD", "EUR"]), only orders whose fiat code is in this list are displayed.
user_mode: Either "user" or "admin". Controls the UI and available actions.ln_address: Optional Lightning address (user@domain.com) used when the local user acts as buyer (receive via LNURL-pay). The embedded template includesln_address = "". Oldersettings.tomlfiles without this key still load (#[serde(default)]yields an empty string). Saving from the Settings tab runs an async check that the LNURL metadata URL returns JSON withtag: "payRequest"before writing disk (spawn_verify_and_save_ln_address_taskinsrc/ui/key_handler/async_tasks.rs, helper insrc/util/ln_address.rs). The spawned task reports onln_address_result_tx(LnAddressVerifyResult), not onorder_result_tx, so settings verification does not share the order/dispute result queue. Clear removes the value without a network call.blossom_servers: Optional list of HTTPS Blossom bases for My Trades attachment upload (Ctrl+O send). When empty, Mostrix usesDEFAULT_BLOSSOM_SERVERSinsrc/util/blossom.rs(same defaults as Mostro Mobile). Example in reposettings.toml: commented# blossom_servers = ["https://blossom.primal.net", …]. Resolved at send time viablossom_servers_from_settingsinsrc/util/send_attachment.rs(main loop reloads settings from disk when draining the send queue).
Proof-of-work for published events is taken from the Mostro instance status event (kind 38385, tag pow), not from settings.toml.
Background and manual refresh (Mostro Info tab → Enter) fetch the daemon status event and update UI state:
AppState.mostro_info: parsed tags (pow,bond_enabled,protocol_version, LND metadata, …) — seemostro_info_from_tags.AppState.transport: resolved wire transport for protocol DMs viatransport_from_instance. Updated throughAppState.set_mostro_info(startup await, main loopMostroInfoFetchResult, reconnect, invalid-pubkey clear).- Startup: when relays are reachable,
run_post_terminal_startupawaitsfetch_mostro_instance_infobefore spawning the DM listener so the first subscription uses the correct transport (v1 GiftWrap or v2 kind 14). On fetch failure or offline boot, transport defaults to GiftWrap.
Displayed on the Mostro Info tab: protocol version (1 / 2 / unknown) and wire transport label (GiftWrap vs NIP-44 direct).
Mostrix initializes a nostr_sdk::Client with the user's keys, adds configured relays, and
connects using a panic-safe wrapper (connect_client_safely).
Current startup behavior:
- Trims relay strings and skips empty entries before adding.
- Computes
relays_reachablewithany_relay_reachablefor offline UI behavior. - Calls
connect_client_safely(&client)(instead of rawclient.connect().await) to prevent background panic crashes when connectivity is unstable. - Logs a warning if no configured relays are reachable at boot.
Several background tasks are spawned to keep the UI and data in sync:
- Order Refresh: Periodically fetches pending orders from Mostro.
- Relay order DB reconcile (startup + ~30s orders updater):
run_relay_order_db_reconcile_once(bulk terminal sync from nostr order events) andrun_targeted_relay_order_db_reconcile_tick(round-robin per-order fetch for local non-terminal trades with keys). Seerelay_order_db_reconcile.rsand MESSAGE_FLOW_AND_PROTOCOL.md (Relay → SQLite section). - Trade Message Listener: Listens for new messages related to active orders.
- Network Status Monitor:
spawn_network_status_monitorruns every 5 seconds.- Re-checks relay reachability from disk settings and emits
NetworkStatus::Offline/Online. - On
Offline, startup overlay text indicates automatic retry. - On
Online,main.rstriggersreload_runtime_session_after_reconnect(...)to reconnect and reload runtime background tasks.
- Shared chat relay poll (
admin_chat_interval, 2 seconds insrc/main.rs):- Admin role: triggers
spawn_admin_chat_fetch→fetch_admin_chat_updates(seesrc/util/order_utils/fetch_scheduler.rs).- For each in-progress dispute, rebuilds per-party shared
Keysfrombuyer_shared_key_hex/seller_shared_key_hexstored in theadmin_disputestable. - Fetches NIP‑59
GiftWrapevents addressed to each shared key's public key (ECDH-derived, same model asmostro-chat). - Uses per‑party
last_seen_timestampvalues to request only new events. - Delegates application of updates to
ui::helpers::apply_admin_chat_updates(implemented insrc/ui/helpers/startup.rs), which:- Appends new
DisputeChatMessageitems intoAppState.admin_dispute_chats. - Persists updated buyer/seller chat cursors in the
admin_disputestable (buyer_chat_last_seen,seller_chat_last_seen).
- Appends new
- For each in-progress dispute, rebuilds per-party shared
- User role: triggers
spawn_user_order_chat_fetch→fetch_user_order_chat_updateson the same timer (shared keys fromorder_chat_shared_key_hexortrade_keys+counterparty_pubkey; applied viaapply_user_order_chat_updates). - A single-flight guard (
CHAT_MESSAGES_SEMAPHORE:AtomicBool) ensures only one shared-key chat fetch runs at a time; overlapping ticks skip spawning a new fetch until the current one completes.
- Admin role: triggers
Source: src/main.rs (background task setup), src/util/order_utils/fetch_scheduler.rs (admin chat scheduler), src/ui/helpers/startup.rs (apply_admin_chat_updates)
- DM Router Wiring (trade messages):
- App channel creation includes
dm_subscription_tx/dm_subscription_rx. set_dm_router_cmd_tx(dm_subscription_tx.clone())publishes the sender globally forwait_for_dm(returnsResult; startup fails fast if the mutex is poisoned).- Before spawning the listener,
hydrate_startup_active_order_dm_stateloads non-terminal orders from SQLite and returnsactive_order_trade_indicesplusorder_last_seen_dm_tscursors;main.rsseeds the shared active-order map. listen_for_order_messages(client, mostro_pubkey, transport, pool, …, order_last_seen_dm_ts, …, dm_subscription_rx)runs as the single router loop consuming:TrackOrdercommands for long-lived trade subscriptions.RegisterWaitercommands for one-shot request/response waits.
- After bootstrapping per-order protocol-DM subscriptions (
ensure_order_dm_subscription), the listener performs afetch_eventsreplay (fetch_and_replay_startup_trade_dms) so the Messages UI is populated from relay history (in-memory messages are not stored in the DB). Replay usesnotify: falseto avoid duplicate popups/badge noise. - Startup transport:
startup.rsawaits instance info when relays are reachable, then spawns the listener with resolvedapp.transport. - Reload / reconnect transport:
dm_transport_for_mostrore-fetches instance info and updatesapp.transportbefore respawning the listener (key reload, fetch-scheduler reload, network reconnect). - This unifies in-flight response handling and background trade notifications on top of one notification stream.
- App channel creation includes
See DM_LISTENER_FLOW.md for DmSubscriptionMode (StartupCatchUp, StartupSince, LiveOnly), filter_protocol_dm_from_mostro, waiter vs TrackOrder ordering, and replay details.
In addition to the background scheduler, Mostrix restores admin chat state during startup:
- All persisted admin disputes are loaded from the
admin_disputestable. - For disputes in
InProgressstate,ui::helpers::recover_admin_chat_from_files:- Reads chat transcripts from
~/.mostrix/disputes_chat/<dispute_id>.txt(if present). - Reconstructs
AppState.admin_dispute_chatsso the "Disputes in Progress" tab immediately shows prior messages. - Updates in‑memory
admin_chat_last_seenentries for Buyer and Seller based on file timestamps.
- Reads chat transcripts from
- Subsequent background NIP‑59 fetches use the stored
buyer_chat_last_seen/seller_chat_last_seenvalues as cursors, ensuring:- Instant UI restore after restart.
- Incremental network sync without replaying the full chat history from relays.
For User role, Mostrix restores peer-to-peer order chat alongside trade DMs:
- Cached transcripts live under
~/.mostrix/orders_chat/<order_id>.txtand are loaded intoAppState.order_chatsbyload_user_order_chats_at_startup. - Attachment rows in transcripts are stored as JSON (
image_encrypted/file_encryptedviaserialize_attachment_for_transcript) so Ctrl+S and file counts work immediately after restart; legacy[Image: … - Ctrl+S to save]lines are hydrated in memory when relay returns the same attachment at the same timestamp. - An immediate relay fetch (
fetch_user_order_chat_updates) merges any newer gift-wrap messages; subsequent polls run every 2 seconds on the sharedadmin_chat_intervaltimer viaspawn_user_order_chat_fetchinsrc/util/order_utils/fetch_scheduler.rs. apply_user_order_chat_updatesskips relay echoes of the local trade pubkey; peer dedup is scoped to existing Peer rows so optimistic You sends are not mirrored as Peer and do not suppress unrelated peer text at the same timestamp. See MESSAGE_FLOW_AND_PROTOCOL.md — "User order chat local cache".
The TUI runs in a tokio::select! loop that handles (among others):
- Fatal errors:
fatal_error_rx— aborts background work and shows an error popup. - Network status:
network_status_rx— offline overlay vs reconnect + runtime reload. - Order / dispute / attachment / observer async results:
order_result_rx—OperationResult; includes dispute-list refresh side effects for certainInfomessages and My Trades DB resync forOrderHistoryDeleted. - Lightning address verify-and-save (settings):
ln_address_result_rx—LnAddressVerifyResult; mapped toOperationResult::Info/Errorand passed tohandle_operation_resultso UI behavior matches other operation-result popups without mixing traffic intoorder_result_rx. - Key rotation / seed words / message notifications / admin & user chat fetches / Mostro instance info / user input / periodic ticks: see
src/main.rs(create_app_channelsinsrc/ui/key_handler/async_tasks.rslists all paired senders and receivers, includingsave_attachment_tx/rxfor Ctrl+S downloads andsend_order_attachment_tx/rxfor outbound My Trades uploads viaSendOrderAttachmentJob). User order chat results arrive onuser_order_chat_updates_rxand are applied viaapply_user_order_chat_updates.
Source: src/main.rs (outer loop + tokio::select! + terminal.draw).
// Simplified shape (not exhaustive — see src/main.rs for full select!)
loop {
tokio::select! {
// fatal_error_rx, network_status_rx, ...
result = order_result_rx.recv() => { apply_order_result(...) }
ln_address_verify = ln_address_result_rx.recv() => { /* map LnAddressVerifyResult → OperationResult */ }
// key_rotation_rx, seed_words_rx, message_notification_rx, ...
maybe_event = events.next() => { /* handle_key_event, paste, mouse */ }
_ = refresh_interval.tick() => { /* 150 ms — redraw even without input */ }
}
// Before every frame (not only on keypress):
drain_save_attachment_queue(...) // start Blossom downloads queued by Ctrl+S popups
drain_send_order_attachment_queue(...) // Ctrl+O / Ctrl+Shift+O jobs: encrypt → Blossom → DM
drain_order_result_queue(...) // apply OperationResult (e.g. "Saved to …", "Attachment sent: …")
expire_attachment_toast(&mut app);
terminal.draw(|f| ui_draw(f, &app, &orders, Some(&status_line)))?;
}
Why drain before draw: My Trades Enter on the save-attachment popup may enqueue the download asynchronously (DB lookup for decryption key). Outbound sends enqueue on send_order_attachment_rx the same way (FromPath or RetryPrepared). Without draining save_attachment_rx, send_order_attachment_rx, and order_result_rx on each frame, success/error popups (including upload-ok/send-failed) could appear only after an unrelated keypress. The 150 ms refresh_interval tick plus this drain keeps attachment save and send feedback timely.