Skip to content

Commit 993556e

Browse files
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 <cursoragent@cursor.com>
1 parent cdc0cb1 commit 993556e

5 files changed

Lines changed: 75 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The **documentation index** is **[docs/README.md](docs/README.md)** — architec
3838

3939
**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)
4040

41-
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).
41+
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).
4242

4343
### Settings (`settings.toml`)
4444

docs/DM_LISTENER_FLOW.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,44 @@ This document explains the runtime flow inside `listen_for_order_messages` (in `
55
- how the in-memory **message list** (`Vec<OrderMessage>`) is created/updated
66
- how “preferences”/routing concepts work: **TrackOrder**, **Waiter**, **Database**, **Action**, **Status**, notifications, and terminal cleanup
77

8-
> **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).
8+
> **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).
9+
10+
## Dual transport (v1 GiftWrap vs v2 NIP-44)
11+
12+
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:
13+
14+
| | **v1** (`protocol_version: "1"` or missing) | **v2** (`protocol_version: "2"`) |
15+
|---|---|---|
16+
| **Subscribe filter** | `.pubkey(trade_key).kind(1059)` | `.author(mostro).pubkey(trade_key).kind(14)` |
17+
| **Inbound event kind** | GiftWrap (1059) | PrivateDirectMessage (14) |
18+
| **Outbound** | GiftWrap via `wrap_message_with` | Signed kind 14 + identity proof |
19+
| **Decrypt** | `unwrap_incoming` (both) | `unwrap_incoming` (both) |
20+
| **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) |
21+
22+
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).
23+
24+
```mermaid
25+
flowchart LR
26+
subgraph discover [Instance info kind 38385]
27+
IV[protocol_version tag]
28+
IV --> T{version}
29+
T -->|1 or missing| GW[Transport::GiftWrap]
30+
T -->|2| N44[Transport::Nip44Direct]
31+
end
32+
33+
subgraph subscribe [Per trade key subscription]
34+
GW --> F1["filter: p=trade, kind 1059"]
35+
N44 --> F2["filter: author=mostro, p=trade, kind 14"]
36+
end
37+
38+
subgraph inbound [Relay event]
39+
F1 --> E1[GiftWrap event]
40+
F2 --> E2[kind-14 event]
41+
E1 --> U[unwrap_incoming]
42+
E2 --> U
43+
U --> R[parse_dm_events → handle_trade_dm_for_order]
44+
end
45+
```
946

1047
## Big picture
1148

@@ -110,7 +147,7 @@ When a relay event arrives (`RelayPoolNotification::Event`) and `event.kind == t
110147

111148
For each waiter:
112149

113-
- test whether `nip59::extract_rumor(&waiter.trade_keys, &event)` succeeds
150+
- test whether [`unwrap_incoming`](../src/util/mod.rs) succeeds for `waiter.trade_keys` and the event
114151
- if it does, send the raw `event` into the waiter oneshot (`response_tx.send(event.clone())`)
115152
- otherwise, keep the waiter pending for the next event
116153

@@ -305,3 +342,13 @@ flowchart TD
305342
- **Terminal detection**: `trade_message_is_terminal`
306343
- **Fallback routing**: `resolve_order_for_event`
307344

345+
## Manual verification (protocol v2)
346+
347+
Use this checklist when validating dual-transport behavior against live nodes:
348+
349+
1. **v1 node** (`protocol_version: "1"`) — create order, take, pay invoice, release; flows unchanged (GiftWrap filters).
350+
2. **v2 node** (`protocol_version: "2"`) — same flows over kind-14 subscribe + `unwrap_incoming`.
351+
3. **Mid-trade restart** — quit and relaunch Mostrix; startup `fetch_events` replay hydrates Messages tab state via the active transport filter.
352+
4. **P2P order chat** — still GiftWrap (`chat_utils.rs`); unaffected by protocol v2 cutover.
353+
5. **Transport flip** (rare) — refresh Mostro Info when `protocol_version` changes; listener respawns with new filter shape.
354+

docs/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Index of architecture and feature guides for the Mostrix TUI client. The [root R
66

77
- **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)
88
- **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`
9-
- **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)
10-
- **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)
9+
- **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)
10+
- **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)
1111
- **Database**: [DATABASE.md](DATABASE.md) — SQLite schema, `orders` / `users` / `admin_disputes`, migrations; **relay → SQLite reconcile** for terminal order statuses (`relay_order_db_reconcile.rs`)
1212
- **Key Management**: [KEY_MANAGEMENT.md](KEY_MANAGEMENT.md) — Deterministic derivation (NIP-06 path), identity vs trade keys
1313

@@ -40,11 +40,11 @@ Index of architecture and feature guides for the Mostrix TUI client. The [root R
4040

4141
## Protocol v2 (NIP-44) — protocol DMs complete
4242

43-
Mostrix is gaining **dual-transport** support for Mostro **protocol DMs** (not P2P order chat or admin dispute chat — those stay on GiftWrap).
43+
Mostrix supports **dual-transport** Mostro **protocol DMs** (not P2P order chat or admin dispute chat — those stay on GiftWrap).
4444

4545
| Status | What |
4646
|--------|------|
47-
| **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 |
47+
| **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)) |
4848

49-
**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.
49+
**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).
5050

src/util/filters.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ mod tests {
7171
assert_eq!(v1.as_json(), legacy.as_json());
7272
}
7373

74+
#[test]
75+
fn filter_protocol_dm_v1_does_not_filter_by_mostro_author() {
76+
let trade = Keys::generate().public_key();
77+
let mostro = Keys::generate().public_key();
78+
let filter = filter_protocol_dm_from_mostro(Transport::GiftWrap, mostro, trade);
79+
let json = filter.as_json();
80+
assert!(json.contains(r#""kinds":[1059]"#));
81+
assert!(json.contains(&format!("\"#p\":[\"{}\"]", trade)));
82+
assert!(!json.contains(&format!(r#""authors":["{}"]"#, mostro)));
83+
}
84+
7485
#[test]
7586
fn filter_protocol_dm_v2_uses_mostro_author_trade_p_tag_and_kind_14() {
7687
let trade = Keys::generate().public_key();

src/util/mostro_info.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,15 @@ mod tests {
505505
);
506506
}
507507

508+
#[test]
509+
fn is_v2_first_contact_protocol_action_matches_new_order_and_takes() {
510+
assert!(is_v2_first_contact_protocol_action(&Action::NewOrder));
511+
assert!(is_v2_first_contact_protocol_action(&Action::TakeBuy));
512+
assert!(is_v2_first_contact_protocol_action(&Action::TakeSell));
513+
assert!(!is_v2_first_contact_protocol_action(&Action::AddInvoice));
514+
assert!(!is_v2_first_contact_protocol_action(&Action::PayInvoice));
515+
}
516+
508517
#[test]
509518
fn parse_pow_first_contact_from_tags() {
510519
let mut tags = Tags::new();

0 commit comments

Comments
 (0)