Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/DATABASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ CREATE TABLE IF NOT EXISTS orders (
The `orders` table is essential for:

- **Trade Key Persistence**: Stores the trade keys needed to decrypt messages and sign actions for each active trade
- **Order Recovery**: Allows the client to recover active orders on startup (`Order::get_startup_active_orders`, `hydrate_startup_active_order_dm_state`)
- **Order Recovery**: Allows the client to recover active orders on startup (`Order::get_startup_active_orders`, `hydrate_startup_active_order_dm_state`) and after **session restore** (`execute_restore_session` → post-restore hydrate in `src/ui/helpers/startup.rs`)
- **State Synchronization**: Enables the "fetch-on-startup" strategy to sync with Mostro daemon
- **Trade History**: Maintains a local record of orders and trades
- **My Trades static header (UI)**: on user history sync, `sync_user_order_history_messages_from_db` in `src/ui/helpers/startup.rs` seeds `AppState.order_chat_static` from existing `orders` rows (`id`, `kind`, `created_at`, `trade_index`, `is_mine`, and trade public key derived from `trade_keys`) so the in-app header (order id, type, created time, trade index, initiator) is stable across process restarts without re-folding the DM list.
Expand Down Expand Up @@ -421,7 +421,7 @@ Mostrix uses a hybrid message recovery strategy that combines stateless fetch-on

- **User order chat (My Trades)**:
- Transcripts under `~/.mostrix/orders_chat/<order_id>.txt` (not in SQLite).
- Same JSON attachment persistence and legacy-placeholder hydration as admin chat; loaded by `load_user_order_chats_at_startup`. See [MESSAGE_FLOW_AND_PROTOCOL.md](MESSAGE_FLOW_AND_PROTOCOL.md) — "User order chat local cache".
- Same JSON attachment persistence and legacy-placeholder hydration as admin chat; loaded by `load_user_order_chats_at_startup` at cold start and by `apply_restored_peer_order_chats_from_disk` after session restore. See [MESSAGE_FLOW_AND_PROTOCOL.md](MESSAGE_FLOW_AND_PROTOCOL.md) — "User order chat local cache" and [STARTUP_AND_CONFIG.md](STARTUP_AND_CONFIG.md) — "Session restore hydrate".

This approach keeps the core trade DM flow largely stateless while giving admin and user order chat a robust, restart‑safe transcript cache.

Expand Down
19 changes: 17 additions & 2 deletions docs/DM_LISTENER_FLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ Relay subscriptions alone often **do not** deliver enough stored history into th

**Practical “where to look”**: `fetch_and_replay_startup_trade_dms`, struct **`DmListenerStartupReplay`**, **`dispatch_giftwrap_batch`** (batch of one at startup), and **`notify`** on **`handle_trade_dm_for_order`** / **`dispatch_giftwrap_batch`** in `src/util/dm_utils/mod.rs`.

### 4) Post-restore trade DM replay (session restore, no restart)

Cold startup replay runs inside `listen_for_order_messages` bootstrap (section 3). **Session restore** (Settings → Restore Session) must refill the Messages tab **without** restarting the listener task. That path is separate:

1. `apply_order_result` handles `OperationResult::SessionRestored` — clears chat projection, re-syncs DB-backed UI rows, then spawns **`spawn_post_restore_hydrate`** (`src/ui/helpers/startup.rs`).
2. **`prepare_post_restore_trade_dm_replay`** reloads `hydrate_startup_active_order_dm_state`, re-seeds `active_order_trade_indices` and `startup_popup_floor_ts` on `AppState`.
3. **`replay_active_trade_dms`** (awaitable; also used from the orchestrator) fetches per active order with **`trade_dm_replay_fetch_filter`**:
- **No `last_seen_dm_ts`** (post-wipe / fresh restore row): **limit-only** — no `since` — so relay retention bounds catch-up (not the 12h cold lookback).
- **Cursor present**: `since` from cursor ∩ lookback, minus GiftWrap envelope skew (`STARTUP_GIFTWRAP_ENVELOPE_SKEW_SECS`), plus fetch limit.
4. Dispatch uses **`UntrackedFallback`** when the live DM router has no `TrackOrder` subscription for the trade pubkey yet (common immediately after restore).
5. Updates `AppState.messages` with **`notify: false`** (no duplicate popups). Completion is folded into **`RestoreHydrateReport.trade_dm`** on **`PostRestoreHydrateCompleted`**.

Peer order chat (My Trades panel) is a **separate pipe** — shared-key kind-14 fetch in `rebuild_peer_order_chats_after_restore`, not the protocol-DM router. See [STARTUP_AND_CONFIG.md](STARTUP_AND_CONFIG.md) — "Session restore hydrate".

## Command “preferences”: TrackOrder vs Waiter

The listener consumes a command channel (`dm_subscription_rx`) with two variants:
Expand Down Expand Up @@ -349,6 +363,7 @@ Use this checklist when validating dual-transport behavior against live nodes:
1. **v1 node** (`protocol_version: "1"`) — create order, take, pay invoice, release; flows unchanged (GiftWrap filters).
2. **v2 node** (`protocol_version: "2"`) — same flows over kind-14 subscribe + `unwrap_incoming`.
3. **Mid-trade restart** — quit and relaunch Mostrix; startup `fetch_events` replay hydrates Messages tab state via the active transport filter.
4. **P2P order chat** — kind 14 outbound (`chat_utils.rs`); inbound still dual-reads legacy GiftWrap while `CHAT_ACCEPT_LEGACY_GIFTWRAP` is true. Unrelated to protocol v2 Mostro DM cutover. Full #102 matrix: [CHAT_KIND14_ACCEPTANCE.md](CHAT_KIND14_ACCEPTANCE.md).
5. **Transport flip** (rare) — refresh Mostro Info when `protocol_version` changes; listener respawns with new filter shape.
4. **Session restore (no restart)** — Settings → Restore Session after seed import; Messages tab and My Trades peer chat hydrate via `spawn_post_restore_hydrate` without relaunching the DM listener. See [RESTORE_SESSION_ACCEPTANCE.md](RESTORE_SESSION_ACCEPTANCE.md).
5. **P2P order chat** — kind 14 outbound (`chat_utils.rs`); inbound still dual-reads legacy GiftWrap while `CHAT_ACCEPT_LEGACY_GIFTWRAP` is true. Unrelated to protocol v2 Mostro DM cutover. Full #102 matrix: [CHAT_KIND14_ACCEPTANCE.md](CHAT_KIND14_ACCEPTANCE.md).
6. **Transport flip** (rare) — refresh Mostro Info when `protocol_version` changes; listener respawns with new filter shape.

3 changes: 2 additions & 1 deletion docs/KEY_MANAGEMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,5 @@ Mostrix avoids storing full message histories locally. Instead, it uses the dete
2. It re-derives the corresponding `Trade Keys`.
3. It queries Nostr relays for recent **protocol DM** events directed to those trade public keys — GiftWrap (kind 1059) or signed kind 14, depending on the Mostro instance `protocol_version` / [`Transport`](../src/util/mod.rs).
4. Separately, **P2P / dispute chat** is hydrated by the shared-key chat router (kind 14 `authors = [pub(K_sign)]`, plus legacy GiftWrap `#p` while `CHAT_ACCEPT_LEGACY_GIFTWRAP` is true).
5. This allows the client to reconstruct the current state of any active trade without needing a heavy local message database.
5. After **session restore** or **seed import / key reload**, `clear_session_chat_projection` clears stale in-memory chat cursors before relay re-hydrate (`src/ui/helpers/startup.rs`, `clear_runtime_session_state` in `src/ui/key_handler/async_tasks.rs`). See [STARTUP_AND_CONFIG.md](STARTUP_AND_CONFIG.md) — "Session restore hydrate".
6. This allows the client to reconstruct the current state of any active trade without needing a heavy local message database.
10 changes: 6 additions & 4 deletions docs/MESSAGE_FLOW_AND_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,14 @@ In addition to relay-driven trade DMs, Mostrix keeps a lightweight local transcr

- **Path**: `~/.mostrix/orders_chat/<order_id>.txt`
- **Startup restore**: `load_user_order_chats_at_startup` restores cached chat into `AppState.order_chats` and seeds `order_chat_last_seen` from on-disk transcripts. Relay backfill is done once by the chat router on `TrackChatKey` after `track_startup_chats` (not a separate poll).
- **Session restore (no restart)**: after Settings → **Restore Session**, `clear_session_chat_projection` clears stale chat maps, DB rows are re-synced to the UI, then **`spawn_post_restore_hydrate`** rebuilds peer transcripts from relay in the background and re-runs `track_startup_chats` on `PostRestoreHydrateCompleted`. See [STARTUP_AND_CONFIG.md](STARTUP_AND_CONFIG.md) — "Session restore hydrate".
- **Live relay sync (User role)**: the **shared-key chat subscription router** (`listen_for_chat_messages` in `src/util/chat_listener.rs`) maintains a batched kind-14 subscription (`authors = [pub(K_sign)]`) over all active order chats and routes by outer author. While `CHAT_ACCEPT_LEGACY_GIFTWRAP` is true, it also dual-reads legacy GiftWrap `#p` = ECDH pubkey. `track_startup_chats` seeds the active-order set at startup; the DM router tracks/untracks orders when the shared key becomes resolvable or the order hits a chat-terminal status ([`TERMINAL_DM_STATUSES`](../src/models.rs) — **`success` keeps chat live**). Dynamic tracks pass a hydrate `since` from the on-disk transcript max timestamp when present. Shared keys come from persisted `order_chat_shared_key_hex` when set, otherwise ECDH from local `trade_keys` + `counterparty_pubkey` (`src/util/chat_utils.rs`).
- **Incremental merge**: `apply_user_order_chat_updates` in `src/ui/helpers/startup.rs`:
- **Skip own relay echoes**: each `OrderChatUpdate` carries `local_trade_pubkey`; messages whose decrypted `sender_pubkey` matches are ignored (same rule as admin chat and Mostro Mobile — avoids showing your send on both **You** and **Peer** after the optimistic local append on Enter).
- **Dedup**: relay self-echoes are skipped by `sender_pubkey == local_trade_pubkey`; peer dedup matches only existing **Peer** rows at the same `(timestamp, content)` (or same attachment / legacy placeholder) so an optimistic **You** line cannot hide a real counterparty message in the same second.
- **Peer-only from relay**: counterparty messages are stored as `UserChatSender::Peer`; local sends are appended as **You** in `handle_enter_user_order_chat` before the relay round-trip.
- Persists new entries with `save_order_chat_message` and advances per-order `order_chat_last_seen`.
- **Peer channel — own relay rows**: each `OrderChatUpdate` carries `local_trade_pubkey`. On **`UserChatChannel::Peer`**, messages from the local trade key are stored as **You** unless the inner event id is already known or an optimistic local **You** line exists at the same timestamp (live-send echo). This allows post-restore relay rebuild to include the user's own history when the on-disk transcript was empty.
- **Solver channel — own relay rows**: still skips all messages whose decrypted `sender_pubkey` matches `local_trade_pubkey` (admin/solver chat convention).
- **Dedup**: inner-event id guards prevent double-writes; peer content dedup matches only existing **Peer** rows at the same `(timestamp, content)` (or same attachment / legacy placeholder) so an optimistic **You** line cannot hide a real counterparty message in the same second.
- **Peer-only from relay (counterparty)**: counterparty messages are stored as `UserChatSender::Peer`; local sends are appended as **You** in `handle_enter_user_order_chat` before the relay round-trip.
- Persists new entries with `save_order_chat_message` / `rewrite_order_chat_messages` and advances per-order `order_chat_last_seen`. Inner event ids are recorded only after durable transcript save succeeds.
- **Attachments (receive + save)**: `image_encrypted` / `file_encrypted` JSON (Mostro Mobile Encrypted File Messaging) is parsed in `apply_user_order_chat_updates` via `try_parse_attachment_message`. Attachment rows show yellow placeholder lines in the chat pane; the block title includes a file count when non-zero; a transient toast notifies on new **peer** files. **Ctrl+S** on My Trades opens `UiMode::UserSaveAttachmentPopup` (pinned `order_id` + list index). Saving downloads from Blossom and decrypts with the attachment key when present, otherwise derives the 32-byte shared secret via `order_chat_decryption_key_bytes` (from `order_chat_shared_key_hex` or ECDH). Files land in `~/.mostrix/downloads/<order_id>_<filename>`.
- **Attachments (send)**: **Ctrl+O** on My Trades opens `UiMode::UserSendAttachmentPicker` (`src/ui/send_attachment_picker.rs`, `ratatui-explorer`) filtered to allowed extensions; **Enter** enqueues `SendOrderAttachmentJob::FromPath`. **Ctrl+Shift+O** retries with `RetryPrepared` when `pending_order_attachment_sends` holds the order. Pipeline in `src/util/send_attachment.rs`:
1. **Validate** local path — `validate_attachment_file` in `src/util/file_validation.rs` (max **25 MB**, extensions `jpg`/`jpeg`/`png`/`pdf`/`mp4`/`mov`/`avi`/`doc`/`docx`, PDF magic-byte check). Images must yield non-zero **width/height** via `read_image_dimensions` (PNG IHDR / JPEG SOF) — required for mobile `image_encrypted` JSON.
Expand Down
9 changes: 5 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ Index of architecture and feature guides for the Mostrix TUI client. The [root R

## Core runtime & data

- **Startup & Configuration**: [STARTUP_AND_CONFIG.md](STARTUP_AND_CONFIG.md) — Boot sequence, settings (`blossom_servers`), background tasks, DM router wiring, reconnect; main loop **drains save/send-attachment and operation-result channels before draw** (150 ms refresh)
- **DM listener & router**: [DM_LISTENER_FLOW.md](DM_LISTENER_FLOW.md) — `listen_for_order_messages`; transport-aware subscribe (`filter_protocol_dm_from_mostro`) and event gate (`transport.event_kind()`); outbound `send_dm` uses `wrap_message_with`; inbound parse uses `unwrap_incoming`
- **Message Flow & Protocol**: [MESSAGE_FLOW_AND_PROTOCOL.md](MESSAGE_FLOW_AND_PROTOCOL.md) — How Mostrix talks to Mostro over Nostr (orders, protocol DMs, restarts, cooperative cancel / `TradeClosed`); **protocol v2** dual transport (`protocol_version` → subscribe, `wrap_message_with`, `unwrap_incoming` — see [Protocol v2 (NIP-44)](#protocol-v2-nip-44--protocol-dms-complete)); **maker bond** (`send_new_order` → `PayBondInvoice` / `PaymentRequestRequired`, deferred `NewOrder` after payment); **My Trades user order chat** relay sync, own-message echo skip, attachment receive/save, **outbound send** (Ctrl+O picker, trade-key Blossom auth, mobile-compatible wire JSON, upload-then-send retry / **Ctrl+Shift+O**, `pending_order_attachment_sends`), **JSON transcript persistence** (Ctrl+S after restart)
- **Startup & Configuration**: [STARTUP_AND_CONFIG.md](STARTUP_AND_CONFIG.md) — Boot sequence, settings (`blossom_servers`), background tasks, DM router wiring, reconnect; **session restore hydrate** (trade-DM + peer-chat rebuild without restart); main loop **drains save/send-attachment and operation-result channels before draw** (150 ms refresh)
- **Session restore acceptance**: [RESTORE_SESSION_ACCEPTANCE.md](RESTORE_SESSION_ACCEPTANCE.md) — post-restore hydrate criteria, automated proofs, manual smoke checklist (step 6)
- **DM listener & router**: [DM_LISTENER_FLOW.md](DM_LISTENER_FLOW.md) — `listen_for_order_messages`; startup and **post-restore** trade-DM replay (`replay_active_trade_dms`, `trade_dm_replay_fetch_filter`); transport-aware subscribe (`filter_protocol_dm_from_mostro`) and event gate (`transport.event_kind()`); outbound `send_dm` uses `wrap_message_with`; inbound parse uses `unwrap_incoming`
- **Message Flow & Protocol**: [MESSAGE_FLOW_AND_PROTOCOL.md](MESSAGE_FLOW_AND_PROTOCOL.md) — How Mostrix talks to Mostro over Nostr (orders, protocol DMs, restarts, cooperative cancel / `TradeClosed`); **protocol v2** dual transport (`protocol_version` → subscribe, `wrap_message_with`, `unwrap_incoming` — see [Protocol v2 (NIP-44)](#protocol-v2-nip-44--protocol-dms-complete)); **maker bond** (`send_new_order` → `PayBondInvoice` / `PaymentRequestRequired`, deferred `NewOrder` after payment); **My Trades user order chat** relay sync, peer-channel echo handling, **post-restore peer transcript rebuild**, attachment receive/save, **outbound send** (Ctrl+O picker, trade-key Blossom auth, mobile-compatible wire JSON, upload-then-send retry / **Ctrl+Shift+O**, `pending_order_attachment_sends`), **JSON transcript persistence** (Ctrl+S after restart)
- **Kind-14 P2P chat acceptance**: [CHAT_KIND14_ACCEPTANCE.md](CHAT_KIND14_ACCEPTANCE.md) — mostrix#102 criteria mapped to automated tests + optional live smoke (closes the gift-wrap apocalypse migration)
- **PoW & outbound events**: [POW_AND_OUTBOUND_EVENTS.md](POW_AND_OUTBOUND_EVENTS.md) — Instance `pow` and optional `pow_first_contact` (kind 38385), [`nostr_pow_for_protocol_dm`](../src/util/mostro_info.rs), [`send_dm`](../src/util/dm_utils/mod.rs) → [`wrap_message_with`](../src/util/mod.rs) (GiftWrap outer PoW or v2 signed kind-14)
- **Database**: [DATABASE.md](DATABASE.md) — SQLite schema, `orders` / `users` / `admin_disputes`, migrations; **relay → SQLite reconcile** for terminal order statuses (`relay_order_db_reconcile.rs`)
- **Key Management**: [KEY_MANAGEMENT.md](KEY_MANAGEMENT.md) — Deterministic derivation (NIP-06 path), identity vs trade keys

## UI & order flows

- **TUI Interface**: [TUI_INTERFACE.md](TUI_INTERFACE.md) — Navigation, modes, state; **Orders** id-based selection + stateful table scroll; **Create New Order** (sectioned form, live preview receipt, searchable currency picker from instance or `currencies.rs`, silent draft persistence, inline validation); **My Trades** (`user_my_trades_interactive`, scroll, receive attachments + Ctrl+S save, **Ctrl+O** send picker + **Ctrl+Shift+O** retry, `order_chat_static` vs live projection); Messages timeline (`StepPendingOrder` = no highlighted column while `Pending` / `WaitingTakerBond` / `WaitingMakerBond`)
- **TUI Interface**: [TUI_INTERFACE.md](TUI_INTERFACE.md) — Navigation, modes, state; **Orders** id-based selection + stateful table scroll; **Create New Order** (sectioned form, live preview receipt, searchable currency picker from instance or `currencies.rs`, silent draft persistence, inline validation); **Settings** → **Restore Session** (post-restore hydrate without restart); **My Trades** (`user_my_trades_interactive`, scroll, receive attachments + Ctrl+S save, **Ctrl+O** send picker + **Ctrl+Shift+O** retry, `order_chat_static` vs live projection); Messages timeline (`StepPendingOrder` = no highlighted column while `Pending` / `WaitingTakerBond` / `WaitingMakerBond`)
- **UI constants** (`src/ui/constants.rs`): Shared copy (footers, help, **`StepLabel`** for the Messages tab buy/sell timeline)
- **Buy order flow (spec)**: [buy order flow.md](buy%20order%20flow.md) — Phase 1.5+ taker bond and Phase 5+ maker bond (`PayBondInvoice` / `WaitingTakerBond` / `WaitingMakerBond`)
- **Sell order flow (spec)**: [sell order flow.md](sell%20order%20flow.md) — Phase 1.5+ taker bond and Phase 5+ maker bond (`PayBondInvoice` / `WaitingTakerBond` / `WaitingMakerBond`)
Expand Down
Loading