From 993556e53e0f5565866b0fb33f3c90bfcd8eac1d Mon Sep 17 00:00:00 2001 From: arkanoider Date: Sat, 20 Jun 2026 15:33:39 +0200 Subject: [PATCH] docs(protocol): complete v2 migration docs and tests (step 9) Document dual-transport subscribe/decrypt flow, manual verification checklist, and v2-complete README copy. Add filter and first-contact action unit tests. Co-authored-by: Cursor --- README.md | 2 +- docs/DM_LISTENER_FLOW.md | 51 ++++++++++++++++++++++++++++++++++++++-- docs/README.md | 10 ++++---- src/util/filters.rs | 11 +++++++++ src/util/mostro_info.rs | 9 +++++++ 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b8dc824..447aa22 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The **documentation index** is **[docs/README.md](docs/README.md)** — architec **Quick links:** [Startup & config](docs/STARTUP_AND_CONFIG.md) · [DM listener / Messages sync](docs/DM_LISTENER_FLOW.md) · [Database](docs/DATABASE.md) · [Message flow & protocol](docs/MESSAGE_FLOW_AND_PROTOCOL.md) · [Key management](docs/KEY_MANAGEMENT.md) · [Coding standards](docs/CODING_STANDARDS.md) -Mostrix reads the connected Mostro instance **`protocol_version`** tag (kind 38385), shows wire transport on the **Mostro Info** tab, and uses transport-aware relay **subscriptions** for protocol DMs; send/decrypt cutover is in progress — see [docs/README.md — Protocol v2](docs/README.md#protocol-v2-nip-44--in-progress). +Mostrix reads the connected Mostro instance **`protocol_version`** tag (kind 38385), **auto-selects** GiftWrap vs NIP-44 for protocol DMs, and shows the resolved wire transport on the **Mostro Info** tab. P2P order chat and admin dispute chat stay on GiftWrap. Details: [docs/README.md — Protocol v2](docs/README.md#protocol-v2-nip-44--protocol-dms-complete). ### Settings (`settings.toml`) diff --git a/docs/DM_LISTENER_FLOW.md b/docs/DM_LISTENER_FLOW.md index 6ace59d..b6491de 100644 --- a/docs/DM_LISTENER_FLOW.md +++ b/docs/DM_LISTENER_FLOW.md @@ -5,7 +5,44 @@ This document explains the runtime flow inside `listen_for_order_messages` (in ` - how the in-memory **message list** (`Vec`) is created/updated - how “preferences”/routing concepts work: **TrackOrder**, **Waiter**, **Database**, **Action**, **Status**, notifications, and terminal cleanup -> **Protocol v2:** Filters use [`filter_protocol_dm_from_mostro`](../src/util/filters.rs) per resolved transport. Outbound [`send_dm`](../src/util/dm_utils/mod.rs) uses `wrap_message_with`; inbound parse and listener decrypt use [`unwrap_incoming`](../src/util/mod.rs). Event gate: `event.kind == transport.event_kind()`. See [Protocol v2](README.md#protocol-v2-nip-44-in-progress). +> **Protocol v2:** Mostrix **auto-selects** wire transport from kind **38385** `protocol_version` (`"1"` → GiftWrap, `"2"` → NIP-44 kind 14). Filters use [`filter_protocol_dm_from_mostro`](../src/util/filters.rs) per resolved [`Transport`](../src/util/mod.rs). Outbound [`send_dm`](../src/util/dm_utils/mod.rs) uses [`wrap_message_with`](../src/util/mod.rs); inbound parse, waiter match, and listener decrypt use [`unwrap_incoming`](../src/util/mod.rs). Event gate: `event.kind == transport.event_kind()`. On transport change after instance-info refresh, [`respawn_trade_dm_listener`](../src/ui/key_handler/async_tasks.rs) restarts this task. See [Protocol v2](README.md#protocol-v2-nip-44--protocol-dms-complete). + +## Dual transport (v1 GiftWrap vs v2 NIP-44) + +Protocol DMs (orders, take, pay, release — **not** P2P order chat or admin dispute chat) share one listener but use different relay filters and event kinds: + +| | **v1** (`protocol_version: "1"` or missing) | **v2** (`protocol_version: "2"`) | +|---|---|---| +| **Subscribe filter** | `.pubkey(trade_key).kind(1059)` | `.author(mostro).pubkey(trade_key).kind(14)` | +| **Inbound event kind** | GiftWrap (1059) | PrivateDirectMessage (14) | +| **Outbound** | GiftWrap via `wrap_message_with` | Signed kind 14 + identity proof | +| **Decrypt** | `unwrap_incoming` (both) | `unwrap_incoming` (both) | +| **PoW** | Instance `pow` on GiftWrap outer | Instance `pow` on signed kind 14; v2 first-contact actions (`NewOrder`, `TakeBuy`, `TakeSell`) use `max(pow, pow_first_contact)` — see [POW_AND_OUTBOUND_EVENTS.md](POW_AND_OUTBOUND_EVENTS.md) | + +Transport is resolved in [`transport_from_instance`](../src/util/mostro_info.rs) and cached on [`AppState.transport`](../src/ui/app_state.rs). Startup **awaits** instance info before spawning the listener; reconnect and Mostro Info refresh reload transport via [`dm_transport_for_mostro`](../src/ui/key_handler/async_tasks.rs). + +```mermaid +flowchart LR + subgraph discover [Instance info kind 38385] + IV[protocol_version tag] + IV --> T{version} + T -->|1 or missing| GW[Transport::GiftWrap] + T -->|2| N44[Transport::Nip44Direct] + end + + subgraph subscribe [Per trade key subscription] + GW --> F1["filter: p=trade, kind 1059"] + N44 --> F2["filter: author=mostro, p=trade, kind 14"] + end + + subgraph inbound [Relay event] + F1 --> E1[GiftWrap event] + F2 --> E2[kind-14 event] + E1 --> U[unwrap_incoming] + E2 --> U + U --> R[parse_dm_events → handle_trade_dm_for_order] + end +``` ## Big picture @@ -110,7 +147,7 @@ When a relay event arrives (`RelayPoolNotification::Event`) and `event.kind == t For each waiter: -- test whether `nip59::extract_rumor(&waiter.trade_keys, &event)` succeeds +- test whether [`unwrap_incoming`](../src/util/mod.rs) succeeds for `waiter.trade_keys` and the event - if it does, send the raw `event` into the waiter oneshot (`response_tx.send(event.clone())`) - otherwise, keep the waiter pending for the next event @@ -305,3 +342,13 @@ flowchart TD - **Terminal detection**: `trade_message_is_terminal` - **Fallback routing**: `resolve_order_for_event` +## Manual verification (protocol v2) + +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** — still GiftWrap (`chat_utils.rs`); unaffected by protocol v2 cutover. +5. **Transport flip** (rare) — refresh Mostro Info when `protocol_version` changes; listener respawns with new filter shape. + diff --git a/docs/README.md b/docs/README.md index f412d18..37fdd4b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,8 +6,8 @@ Index of architecture and feature guides for the Mostrix TUI client. The [root R - **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** (`protocol_version` → outbound `wrap_message_with`; inbound still partial — see [Protocol v2 (NIP-44)](#protocol-v2-nip-44-in-progress)); **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) -- **PoW & outbound events**: [POW_AND_OUTBOUND_EVENTS.md](POW_AND_OUTBOUND_EVENTS.md) — Instance `pow` (kind 38385), `nostr_pow_from_instance`, [`send_dm`](../src/util/dm_utils/mod.rs) → [`wrap_message_with`](../src/util/mod.rs) (GiftWrap outer PoW or v2 signed kind-14) +- **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) +- **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 @@ -40,11 +40,11 @@ Index of architecture and feature guides for the Mostrix TUI client. The [root R ## Protocol v2 (NIP-44) — protocol DMs complete -Mostrix is gaining **dual-transport** support for Mostro **protocol DMs** (not P2P order chat or admin dispute chat — those stay on GiftWrap). +Mostrix supports **dual-transport** Mostro **protocol DMs** (not P2P order chat or admin dispute chat — those stay on GiftWrap). | Status | What | |--------|------| -| **Done** | `mostro-core` **0.13.0**; `protocol_version` on kind **38385**; [`transport_from_instance`](../src/util/mostro_info.rs); [`AppState.transport`](../src/ui/app_state.rs); Mostro Info tab; [`filter_protocol_dm_from_mostro`](../src/util/filters.rs); **await instance info** before listener (startup + [`dm_transport_for_mostro`](../src/ui/key_handler/async_tasks.rs) on reload/reconnect); **`send_dm` → `wrap_message_with`**; **`parse_dm_events` / listener → `unwrap_incoming`**; transport-aware subscribe + event gate; [`respawn_trade_dm_listener`](../src/ui/key_handler/async_tasks.rs) on manual info refresh when transport flips | +| **Done** | `mostro-core` **0.13.0**; `protocol_version` on kind **38385**; [`transport_from_instance`](../src/util/mostro_info.rs); [`AppState.transport`](../src/ui/app_state.rs); Mostro Info tab; [`filter_protocol_dm_from_mostro`](../src/util/filters.rs); **await instance info** before listener (startup + [`dm_transport_for_mostro`](../src/ui/key_handler/async_tasks.rs) on reload/reconnect); **`send_dm` → `wrap_message_with`**; **`parse_dm_events` / listener → `unwrap_incoming`**; transport-aware subscribe + event gate; [`respawn_trade_dm_listener`](../src/ui/key_handler/async_tasks.rs) on manual info refresh when transport flips; v2 **first-contact PoW** (`pow_first_contact` / [`nostr_pow_for_protocol_dm`](../src/util/mostro_info.rs)) | -**v2 end-to-end:** Mostrix auto-selects wire transport from instance info for both outbound and inbound protocol DMs. P2P order chat and admin dispute chat remain GiftWrap-only. +**v2 end-to-end:** Mostrix auto-selects wire transport from instance info for both outbound and inbound protocol DMs. P2P order chat and admin dispute chat remain GiftWrap-only. Manual test checklist: [DM_LISTENER_FLOW.md — Manual verification](DM_LISTENER_FLOW.md#manual-verification-protocol-v2). diff --git a/src/util/filters.rs b/src/util/filters.rs index e5ed2b1..7f66621 100644 --- a/src/util/filters.rs +++ b/src/util/filters.rs @@ -71,6 +71,17 @@ mod tests { assert_eq!(v1.as_json(), legacy.as_json()); } + #[test] + fn filter_protocol_dm_v1_does_not_filter_by_mostro_author() { + let trade = Keys::generate().public_key(); + let mostro = Keys::generate().public_key(); + let filter = filter_protocol_dm_from_mostro(Transport::GiftWrap, mostro, trade); + let json = filter.as_json(); + assert!(json.contains(r#""kinds":[1059]"#)); + assert!(json.contains(&format!("\"#p\":[\"{}\"]", trade))); + assert!(!json.contains(&format!(r#""authors":["{}"]"#, mostro))); + } + #[test] fn filter_protocol_dm_v2_uses_mostro_author_trade_p_tag_and_kind_14() { let trade = Keys::generate().public_key(); diff --git a/src/util/mostro_info.rs b/src/util/mostro_info.rs index 1b61212..42d93e7 100644 --- a/src/util/mostro_info.rs +++ b/src/util/mostro_info.rs @@ -505,6 +505,15 @@ mod tests { ); } + #[test] + fn is_v2_first_contact_protocol_action_matches_new_order_and_takes() { + assert!(is_v2_first_contact_protocol_action(&Action::NewOrder)); + assert!(is_v2_first_contact_protocol_action(&Action::TakeBuy)); + assert!(is_v2_first_contact_protocol_action(&Action::TakeSell)); + assert!(!is_v2_first_contact_protocol_action(&Action::AddInvoice)); + assert!(!is_v2_first_contact_protocol_action(&Action::PayInvoice)); + } + #[test] fn parse_pow_first_contact_from_tags() { let mut tags = Tags::new();