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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
51 changes: 49 additions & 2 deletions docs/DM_LISTENER_FLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,44 @@ This document explains the runtime flow inside `listen_for_order_messages` (in `
- how the in-memory **message list** (`Vec<OrderMessage>`) 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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.

10 changes: 5 additions & 5 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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).

11 changes: 11 additions & 0 deletions src/util/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions src/util/mostro_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down