Skip to content

Commit 3b4e585

Browse files
committed
feat: feat(getdmuser): show shared-key DMs (identity + admin) in getdmuser - Derive shared key from (trade_keys, identity_keys) and fetch/unwrap gift wraps so DMs sent to the user's identity appear in getdmuser. - Also derive (trade_keys, mostro_pubkey) and fetch so admin replies from the send_admin_dm_attach flow are shown. - Reuse derive_shared_key_bytes from util/messaging (same ECDH as send_admin_dm_attach). No longer use get_all_trade_and_counterparty_keys; counterparty_pubkey is not used in this setup.
1 parent dbeb351 commit 3b4e585

21 files changed

Lines changed: 413 additions & 171 deletions

.cursor/commands/build.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# build mostro-cli using coding standards
2+
3+
4+
## Overview
5+
6+
Build and test mostro-cli, fixing all errors reported by cargo and clippy.
7+
8+
## Steps
9+
10+
- execute cargo fmt --all
11+
- execute cargo clippy --all-targets --all-features
12+
- execute cargo test
13+
- execute cargo build

.cursor/commands/pull_request.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Create PR
2+
3+
## Overview
4+
5+
Create a well-structured pull request with proper description, labels, and reviewers.
6+
7+
## Steps
8+
9+
1. **Prepare branch**
10+
- Ensure all changes are committed
11+
- Push branch to remote
12+
- Verify branch is up to date with main
13+
14+
2. **Write PR description**
15+
- Summarize changes clearly
16+
- Include context and motivation
17+
- List any breaking changes
18+
- Add screenshots if UI changes
19+
20+
3. **Set up PR**
21+
- Create PR with descriptive title
22+
- Add appropriate labels
23+
- Assign reviewers
24+
- Link related issues
25+
26+
## PR Template
27+
28+
- [ ] Feature A
29+
- [ ] Bug fix B
30+
- [ ] Unit tests pass
31+
- [ ] Manual testing completed

.cursor/commands/update_docs.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Update AI docs for automatic code generation with context
2+
3+
## Overview
4+
5+
Keep Markdown documents updated to provide AI context, improving the quality of generated code.
6+
7+
## Steps
8+
9+
- Identify the latest changes in Git history for files in the docs folder
10+
- Analyze all new changes up to the latest commit
11+
- Document new features, fixes, and refactorings in the docs
12+
- Add contextual notes for structural changes (e.g., update DATABASE.md for DB schema changes)

docs/architecture.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ This document describes the internal structure of `mostro-cli`, how major module
3131
- **`src/util/mod.rs`**
3232
- Organizes utility modules:
3333
- `events`: event filtering and retrieval from Nostr.
34-
- `messaging`: higher-level DM helpers (gift-wrapped messages, admin keys, etc.).
34+
- `messaging`: higher-level DM helpers (gift-wrapped messages, admin keys, **shared-key derivation and custom wraps**).
3535
- `misc`: small helpers such as `get_mcli_path` and string utilities.
3636
- `net`: Nostr network connection setup.
3737
- `storage`: thin storage helpers for orders and DMs.
3838
- `types`: small shared enums/wrappers.
39-
- Re-exports commonly used symbols (`create_filter`, `send_dm`, `connect_nostr`, `save_order`, etc.) so other modules can import from `crate::util` directly.
39+
- Re-exports commonly used symbols (`create_filter`, `send_dm`, `connect_nostr`, `save_order`, **`derive_shared_keys`, `derive_shared_key_hex`, `keys_from_shared_hex`, `send_admin_chat_message_via_shared_key`**, etc.) so other modules can import from `crate::util` directly.
4040

4141
- **`src/util/storage.rs`**
4242
- `save_order(order, trade_keys, request_id, trade_index, pool)`:
@@ -68,7 +68,7 @@ This document describes the internal structure of `mostro-cli`, how major module
6868
- Handles creation (`User::new`), loading (`User::get`), updating (`save`), and key derivation helpers (identity keys and per-trade keys using `nip06`).
6969
- `Order`:
7070
- Represents cached orders with fields mapped to the `orders` table.
71-
- Provides `new`, `insert_db`, `update_db`, fluent setters, `save`, `save_new_id`, `get_by_id`, `get_all_trade_keys`, and `delete_by_id`.
71+
- Provides `new`, `insert_db`, `update_db`, fluent setters, `save`, `save_new_id`, `get_by_id`, `get_all_trade_keys`, **`get_all_trade_and_counterparty_keys`** (distinct `(trade_keys, counterparty_pubkey)` pairs for orders where both are set), and `delete_by_id`.
7272
- See `database.md` for schema details.
7373

7474
### Parsers and protocol types
@@ -82,6 +82,10 @@ This document describes the internal structure of `mostro-cli`, how major module
8282
- `common.rs`: shared parsing helpers.
8383
- `mod.rs`: module glue.
8484

85+
- **Shared-key custom wraps** (`src/util/messaging.rs`):
86+
- **Sending**: `derive_shared_keys(local_keys, counterparty_pubkey)` yields a `Keys` whose public key is used as the NIP-59 gift-wrap recipient; inner content is a signed text note encrypted with NIP-44 to that pubkey. Used by `dmtouser` and `sendadmindmattach`.
87+
- **Receiving**: `unwrap_giftwrap_with_shared_key(shared_keys, event)` decrypts with NIP-44 and returns `(content, timestamp, sender_pubkey)`; `fetch_gift_wraps_for_shared_key(client, shared_keys)` fetches Kind::GiftWrap events with `#p` = shared key pubkey and unwraps them. Use when implementing flows that read shared-key DMs.
88+
8589
### Lightning integration
8690

8791
- **`src/lightning/mod.rs`**

docs/commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ All commands are part of the `Commands` enum and are dispatched via `Commands::r
110110
- **Handler**: `execute_send_dm(PublicKey::from_str(pubkey)?, ctx, order_id, &msg)` in `src/cli/send_dm.rs`.
111111

112112
- **`dmtouser`**
113-
- **Description**: Send a gift-wrapped direct message to a user.
113+
- **Description**: Send a direct message to a user via a **shared-key custom wrap**. Derives an ECDH shared key from the order’s trade keys and the recipient pubkey; the message is sent as a NIP-59 gift wrap addressed to the shared key’s public key (NIP-44 encrypted), so both sides can decrypt.
114114
- **Args**:
115115
- `--pubkey <NPUB/HEX>`: Recipient pubkey.
116-
- `--order-id <UUID>`: Order id to derive ephemeral keys.
116+
- `--order-id <UUID>`: Order id to derive trade keys and shared key.
117117
- `--message <STRING>...`: Message parts; joined with spaces.
118118
- **Handler**: `execute_dm_to_user(PublicKey::from_str(pubkey)?, &ctx.client, order_id, &msg, &ctx.pool)` in `src/cli/dm_to_user.rs`.
119119

docs/database.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ This file is created under the CLI data directory returned by `util::get_mcli_pa
7777
- Loads a single order (and returns an error if no ID is present).
7878
- `get_all_trade_keys(pool)`:
7979
- Returns distinct non-null `trade_keys` for all orders.
80+
- `get_all_trade_and_counterparty_keys(pool)`:
81+
- Returns distinct `(trade_keys, counterparty_pubkey)` pairs for orders where both columns are non-null; useful for deriving per-order shared keys when fetching or sending shared-key DMs.
8082
- `delete_by_id(pool, id)`:
8183
- Deletes an order row.
8284

docs/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ The CLI is heavily inspired by the Mostro backend documentation (`mostro/docs` i
77
### High-level responsibilities
88

99
- **Order lifecycle**: create, take, cancel, dispute, and settle orders using `mostro_core` types and the Mostro protocol.
10-
- **Direct messaging**: send and receive Nostr DMs between users, admins, and solvers, including gift-wrapped messages and encrypted attachments.
10+
- **Direct messaging**: send and receive Nostr DMs between users, admins, and solvers, including gift-wrapped messages, shared-key custom wraps (ECDH-derived key, NIP-44 inside NIP-59) for `dmtouser` and admin attachment DMs, and encrypted attachments.
1111
- **Local persistence**: keep a local cache of orders and a deterministic identity in a SQLite database (`mcli.db`) under the CLI data directory.
1212
- **Admin / solver tooling**: expose admin-only and solver-only flows (e.g. taking disputes, adding solvers, admin DMs) when run with the proper keys.
1313

docs/send_admin_dm_attach_flow.md

Lines changed: 21 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -320,115 +320,30 @@ Blossom protocol details:
320320

321321
The function ultimately returns a **public URL** (`blossom_url`) pointing to the encrypted blob.
322322

323-
### 5. DM payload and gift wrap to admin
324-
325-
Once the encrypted blob is uploaded and we have `blossom_url`, the DM payload is built:
326-
327-
```268:311:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs
328-
let filename = file_path
329-
.file_name()
330-
.and_then(|s| s.to_str())
331-
.unwrap_or("attachment.bin")
332-
.to_string();
333-
334-
// Best-effort MIME type detection based on the file extension.
335-
let mime_type = file_path
336-
.extension()
337-
.and_then(|ext| ext.to_str())
338-
.map(|ext| ext.to_ascii_lowercase())
339-
.map(|ext| match ext.as_str() {
340-
"txt" => "text/plain",
341-
"md" => "text/markdown",
342-
"json" => "application/json",
343-
"csv" => "text/csv",
344-
"jpg" | "jpeg" => "image/jpeg",
345-
"png" => "image/png",
346-
"gif" => "image/gif",
347-
"webp" => "image/webp",
348-
"pdf" => "application/pdf",
349-
"zip" => "application/zip",
350-
"tar" => "application/x-tar",
351-
"gz" | "tgz" => "application/gzip",
352-
"mp3" => "audio/mpeg",
353-
"mp4" => "video/mp4",
354-
"mov" => "video/quicktime",
355-
_ => "application/octet-stream",
356-
})
357-
.unwrap_or("application/octet-stream")
358-
.to_string();
359-
360-
let payload_json = serde_json::json!({
361-
"type": "file_encrypted",
362-
"blossom_url": blossom_url,
363-
"nonce": nonce_hex,
364-
"mime_type": mime_type,
365-
"original_size": file_bytes.len(),
366-
"filename": filename,
367-
"encrypted_size": encrypted_size,
368-
"file_type": "document",
369-
});
370-
371-
let content = serde_json::to_string(&payload_json)
372-
.map_err(|e| anyhow::anyhow!("failed to serialize attachment payload: {e}"))?;
373-
```
374-
375-
This JSON is the **Mostro DM payload** describing the encrypted attachment:
376-
377-
- `type = "file_encrypted"` – payload kind.
378-
- `blossom_url` – where the admin can fetch the encrypted blob.
379-
- `nonce` – hex nonce for ChaCha20‑Poly1305.
380-
- `mime_type` – hint about original file type.
381-
- `original_size` / `encrypted_size` – size bookkeeping.
382-
- `filename` – original filename.
383-
384-
#### 5.1 Gift‑wrapped DM event
385-
386-
```316:331:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs
387-
let pow: u8 = std::env::var("POW")
388-
.unwrap_or_else(|_| "0".to_string())
389-
.parse()
390-
.unwrap_or(0);
391-
392-
let rumor = EventBuilder::text_note(content)
393-
.pow(pow)
394-
.build(trade_keys.public_key());
395-
396-
let event = EventBuilder::gift_wrap(&trade_keys, &receiver, rumor, Tags::new()).await?;
397-
398-
ctx.client
399-
.send_event(&event)
400-
.await
401-
.map_err(|e| anyhow::anyhow!("failed to send gift wrap event: {e}"))?;
402-
403-
println!("✅ Encrypted attachment sent successfully to admin!");
404-
```
323+
### 5. DM payload and shared-key custom wrap to admin
405324

406-
- **Rumor event** (inner):
407-
- `kind`: `TextNote`
408-
- `content`: the `payload_json` string above.
409-
- `pubkey`: `trade_keys.public_key()` (per‑order trade identity).
410-
- Optional POW from `POW` env var.
325+
Once the encrypted blob is uploaded and we have `blossom_url`, the DM payload is built as JSON (`type`, `blossom_url`, `nonce`, `mime_type`, sizes, `filename`). This content is then sent using **shared-key custom wrap** (same pattern as `dmtouser`):
411326

412-
- **Outer GiftWrap event** (NIP‑59):
413-
- Created via `EventBuilder::gift_wrap(&trade_keys, &receiver, rumor, Tags::new())`.
414-
- **Signer / sender**: `trade_keys` (per‑order).
415-
- **Recipient**: `receiver` (admin / solver Nostr pubkey).
416-
- **Tags**: currently empty (no extra expiration here; optional).
327+
- **Shared key**: The same ECDH shared key used for file encryption (trade keys + admin pubkey) is turned into a `Keys` via `Keys::new(SecretKey::from_slice(&shared_key)?)`.
328+
- **Send**: `send_admin_chat_message_via_shared_key(&ctx.client, &trade_keys, &shared_keys, &content)` in `src/util/messaging.rs`:
329+
- Builds an inner text-note event (sender = trade keys), signs it, encrypts it with NIP-44 to the **shared key’s public key**, and wraps it in a NIP-59 GiftWrap event tagged with that pubkey (`#p`).
330+
- Both the sender (trade keys) and the admin (who can derive the same shared key) can later fetch and decrypt the event by filtering GiftWrap by the shared key pubkey and using `unwrap_giftwrap_with_shared_key`.
417331

418-
- **Relaying**:
419-
- Final event is sent with `ctx.client.send_event(&event).await`.
420-
- `ctx.client` is connected to configured Nostr relays via `RELAYS` env var.
332+
So the attachment metadata is not sent as a plain NIP-59 gift wrap to the admin pubkey; it is sent to the **shared key’s public key**, enabling symmetric decryption for both parties. Relaying is via `ctx.client.send_event(&event)`.
421333

422334
### 6. Keys and protocols summary
423335

424336
- **Keys**:
425337
- `trade_keys` (per‑order):
426338
- Used for:
427-
- ECDH shared secret with admin pubkey for file encryption.
428-
- Nostr DM identity for the rumor + giftwrap.
429-
- (Indirectly) for signing Blossom auth event (kind 24242).
339+
- ECDH shared secret with admin pubkey (for file encryption and for the shared-key DM).
340+
- Nostr identity for the inner text note and for signing the outer NIP‑59 wrap.
341+
- Signing the Blossom auth event (kind 24242).
342+
- **Shared key** (ECDH from trade_keys + admin pubkey):
343+
- Same 32-byte secret used for ChaCha20‑Poly1305 file encryption.
344+
- Wrapped as `Keys` and used as the **recipient** of the DM: the NIP-59 GiftWrap is addressed to the shared key’s public key, and the inner content is NIP-44 encrypted to it, so both sender and admin can derive the key and decrypt.
430345
- `receiver`:
431-
- Admin / solver Nostr pubkey; DM destination for the final NIP‑59 GiftWrap.
346+
- Admin / solver Nostr pubkey; used to derive the shared key and as the human-facing destination (the actual Nostr event recipient is the shared key pubkey).
432347

433348
- **Protocols**:
434349
- **ChaCha20‑Poly1305**:
@@ -441,16 +356,15 @@ println!("✅ Encrypted attachment sent successfully to admin!");
441356
- JSON with attachment metadata:
442357
- `type`, `blossom_url`, `nonce`, `mime_type`, sizes, `filename`.
443358
- **Nostr**:
444-
- NIP‑13: optional POW on the rumor text note.
445-
- NIP‑40: optional expiration (not used on the DM here).
446-
- NIP‑59: GiftWrap envelope:
447-
- Wraps the text‑note rumor for the admin’s pubkey.
359+
- NIP‑13: optional POW on the inner text note.
360+
- NIP‑44: encryption of the inner event to the shared key’s public key.
361+
- NIP‑59: GiftWrap envelope addressed to the **shared key pubkey** (not directly to the admin), so both parties that know the ECDH secret can fetch and decrypt.
448362

449363
End‑to‑end, `sendadmindmattach`:
450364

451-
1. Derives a shared ECDH key between trade and admin.
452-
2. Encrypts the file with ChaCha20‑Poly1305.
365+
1. Derives a shared ECDH key between trade keys and admin pubkey.
366+
2. Encrypts the file with ChaCha20‑Poly1305 using that key.
453367
3. Authenticates to Blossom with a kind‑24242 auth event (BUD‑01) and uploads the encrypted blob (BUD‑02).
454-
4. Builds a Mostro DM payload with Blossom URL + crypto metadata.
455-
5. Sends a NIP‑59 gift‑wrapped text note from the trade keys to the admin pubkey with that payload as content.
368+
4. Builds a Mostro DM payload JSON with Blossom URL and crypto metadata.
369+
5. Sends a **shared-key custom wrap** (NIP-44 inner content, NIP-59 GiftWrap addressed to the shared key’s public key) via `send_admin_chat_message_via_shared_key`, so both the sender and the admin can decrypt the attachment metadata and fetch the blob.
456370

src/cli/add_invoice.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context)
4343
));
4444
println!("{table}");
4545
println!("💡 Sending lightning invoice to Mostro...\n");
46-
// Check invoice string
46+
// Parse invoice (Lightning address or BOLT11) and build payload
4747
let ln_addr = LightningAddress::from_str(invoice);
4848
let payload = if ln_addr.is_ok() {
4949
Some(Payload::PaymentRequest(None, invoice.to_string(), None))

src/cli/dm_to_user.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use crate::parser::common::{
22
print_info_line, print_key_value, print_section_header, print_success_message,
33
};
4-
use crate::{db::Order, util::send_gift_wrap_dm};
4+
use crate::{
5+
db::Order,
6+
util::{derive_shared_keys, send_admin_chat_message_via_shared_key},
7+
};
58
use anyhow::Result;
69
use nostr_sdk::prelude::*;
710
use sqlx::SqlitePool;
@@ -24,16 +27,26 @@ pub async fn execute_dm_to_user(
2427
None => anyhow::bail!("No trade_keys found for this order"),
2528
};
2629

27-
// Send the DM
30+
// Derive per-dispute shared keys between our trade keys and the receiver pubkey
31+
let shared_keys = derive_shared_keys(Some(&trade_keys), Some(&receiver))
32+
.ok_or_else(|| anyhow::anyhow!("Failed to derive shared key for this DM"))?;
33+
34+
// Print summary and send shared-key wrap DM
2835
print_section_header("💬 Direct Message to User");
2936
print_key_value("📋", "Order ID", &order_id.to_string());
3037
print_key_value("🔑", "Trade Keys", &trade_keys.public_key().to_hex());
3138
print_key_value("🎯", "Recipient", &receiver.to_string());
3239
print_key_value("💬", "Message", message);
33-
print_info_line("💡", "Sending gift wrap message...");
40+
print_key_value(
41+
"🔑",
42+
"Shared Key Pubkey",
43+
&shared_keys.public_key().to_hex(),
44+
);
45+
print_info_line("💡", "Sending shared-key custom wrap message...");
3446
println!();
3547

36-
send_gift_wrap_dm(client, &trade_keys, &receiver, message).await?;
48+
// Send as shared-key custom wrap so both parties can decrypt via the shared key
49+
send_admin_chat_message_via_shared_key(client, &trade_keys, &shared_keys, message).await?;
3750

3851
print_success_message("Gift wrap message sent successfully!");
3952

0 commit comments

Comments
 (0)