mostro_core/chat/wrap.rs
1//! Build a Mostro P2P chat gift wrap.
2//!
3//! The wire format is a non-standard NIP-59 envelope: a kind 1 text note
4//! signed by the sender's trade key is encrypted with NIP-44 v2 using an
5//! ephemeral key as the encryption side and the **shared key's public key**
6//! as the recipient. The outer kind 1059 event carries that ciphertext, a
7//! `p` tag pointing at the shared pubkey for relay routing, and is signed
8//! with the same ephemeral key.
9//!
10//! See <https://mostro.network/protocol/chat.html> for the protocol spec.
11
12use nostr_sdk::nips::{nip44, nip59};
13use nostr_sdk::prelude::*;
14
15use crate::error::{MostroError, ServiceError};
16
17/// Wrap a plain-text chat message into a Mostro P2P gift wrap (kind 1059).
18///
19/// * `sender_trade_keys` — the sender's per-trade keys; sign the inner kind 1
20/// so the receiver can verify authorship.
21/// * `shared_pubkey` — public key of the [`SharedKey`](super::SharedKey)
22/// shared by the two chat parties; used as the NIP-44 recipient and as
23/// the value of the outer `p` tag.
24/// * `message` — the chat content to deliver.
25///
26/// The outer `created_at` is randomized within
27/// [`nip59::RANGE_RANDOM_TIMESTAMP_TWEAK`] to defeat timing correlation.
28pub async fn wrap_chat_message(
29 sender_trade_keys: &Keys,
30 shared_pubkey: &PublicKey,
31 message: &str,
32) -> Result<Event, MostroError> {
33 let inner = EventBuilder::text_note(message)
34 .build(sender_trade_keys.public_key())
35 .sign(sender_trade_keys)
36 .await
37 .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
38
39 let ephemeral = Keys::generate();
40 let encrypted = nip44::encrypt(
41 ephemeral.secret_key(),
42 shared_pubkey,
43 inner.as_json(),
44 nip44::Version::V2,
45 )
46 .map_err(|e| MostroError::MostroInternalErr(ServiceError::EncryptionError(e.to_string())))?;
47
48 EventBuilder::new(Kind::GiftWrap, encrypted)
49 .tag(Tag::public_key(*shared_pubkey))
50 .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK))
51 .sign_with_keys(&ephemeral)
52 .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))
53}