Skip to main content

mostro_core/
transport.rs

1//! Transport selection for Mostro protocol messages.
2//!
3//! Mostro supports two wire transports for the same logical
4//! [`Message`](crate::message::Message):
5//!
6//! * **Protocol v1 — NIP-59 GiftWrap** (`kind: 1059`, see [`crate::nip59`]):
7//!   fully opaque envelopes. Strong metadata privacy, but relays cannot
8//!   rate-limit by sender, which makes the node spam-prone. **DEPRECATED**
9//!   (see [`Transport::GiftWrap`]): mostrod v0.19.0 drops it and runs
10//!   protocol v2 only —
11//!   <https://github.com/MostroP2P/mostro/issues/786>.
12//! * **Protocol v2 — NIP-44 direct** (`kind: 14`): a *signed* event authored
13//!   by the per-trade key, whose `content` is the NIP-44 encryption of a
14//!   3-element JSON tuple:
15//!
16//!   ```text
17//!   [Message, trade_sig | null, [identity_pubkey, identity_sig] | null]
18//!   ```
19//!
20//!   The visible sender lets relays and the daemon rate-limit and
21//!   pre-filter cheaply, while the identity key — and its proof of
22//!   possession — stays inside the ciphertext, exactly as private as the
23//!   seal makes it in v1. `trade_sig` is [`Message::sign`] over the JSON
24//!   of the first tuple element; `identity_sig` signs a domain-tagged
25//!   payload that includes the trade pubkey
26//!   (`mostro-transport-v2-identity:<trade_pubkey>:<message_json>`), so
27//!   the proof binds the identity to the specific trade key authoring the
28//!   event and cannot be grafted onto another sender. A `null` third
29//!   element is full-privacy mode: the identity is the trade key itself.
30//!
31//! Note the deliberate deviation from NIP-17: there, `kind: 14` is an
32//! *unsigned* rumor that only travels inside a gift wrap. Mostro publishes
33//! it signed because the author is an ephemeral, single-trade key, so the
34//! association the NIP-17 rule protects against is intentional and bounded.
35//!
36//! A node speaks exactly one transport, chosen by the operator: v1 *or*
37//! v2, never both at once. Both transports still unwrap into the same
38//! [`UnwrappedMessage`], so consumers never need to know which envelope a
39//! message arrived in — [`unwrap_incoming`] dispatches on the event kind,
40//! which also lets one client implementation talk to v1 and v2 nodes.
41
42use std::str::FromStr;
43
44use crate::message::Message;
45use crate::nip59::{self, UnwrappedMessage, WrapOptions};
46use crate::prelude::{MostroError, ServiceError};
47use nostr_sdk::nips::nip44;
48use nostr_sdk::prelude::*;
49use serde::{Deserialize, Serialize};
50
51/// Inner content of a v2 event, before NIP-44 encryption:
52/// `[Message, trade_sig, [identity_pubkey, identity_sig]]`.
53type DirectTuple = (Message, Option<String>, Option<(String, String)>);
54
55/// Payload the identity key signs for the v2 identity proof.
56///
57/// It is domain-tagged and includes the trade pubkey, so the proof binds
58/// the identity to *both* the message and the trade key that authored the
59/// event — the binding v1 gets from the seal signature covering the
60/// encrypted rumor (which carries `rumor.pubkey`). Signing the message
61/// JSON alone would let any party that sees the plaintext tuple graft the
62/// proof onto an event authored by a different trade key.
63fn identity_proof_payload(trade_pubkey: &PublicKey, message_json: &str) -> String {
64    format!(
65        "mostro-transport-v2-identity:{}:{}",
66        trade_pubkey.to_hex(),
67        message_json
68    )
69}
70
71/// The wire transport a node speaks. A node runs exactly one: the
72/// operator picks protocol v1 *or* v2 via the `transport` setting
73/// (`"gift-wrap"` or `"nip44"`), and every message — inbound subscription
74/// and outbound replies — uses that transport.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76pub enum Transport {
77    /// Protocol v1 — NIP-59 GiftWrap (`kind: 1059`).
78    ///
79    /// DEPRECATED(mostro#786): protocol v1 is being phased out. mostrod
80    /// v0.19.0 removes its `transport` setting and runs protocol v2
81    /// (`nip44`) only; this variant will be removed from mostro-core in a
82    /// following release, once the ecosystem completes the v2 migration.
83    /// Clients may still need it meanwhile to talk to pre-v2 nodes.
84    #[deprecated(
85        note = "protocol v1 (gift-wrap) is being phased out — mostrod v0.19.0 runs protocol v2 (nip44) only; see https://github.com/MostroP2P/mostro/issues/786"
86    )]
87    #[serde(rename = "gift-wrap")]
88    GiftWrap,
89    /// Protocol v2 — NIP-44 direct message (`kind: 14`).
90    #[serde(rename = "nip44")]
91    Nip44Direct,
92}
93
94/// Manual impl (not `#[derive(Default)]` + `#[default]`) because the default
95/// variant is the deprecated one and the derive would trip the deprecation
96/// lint at the definition site. The default stays `GiftWrap` for backward
97/// compatibility until mostrod v0.19.0 flips the ecosystem to v2 only.
98impl Default for Transport {
99    #[allow(deprecated)]
100    fn default() -> Self {
101        Transport::GiftWrap
102    }
103}
104
105// The variant stays fully functional until removal, so the enum's own
106// impls keep matching on it without tripping the deprecation lint.
107#[allow(deprecated)]
108impl Transport {
109    /// The Nostr event kind this transport publishes and subscribes to.
110    pub fn event_kind(&self) -> Kind {
111        match self {
112            Transport::GiftWrap => Kind::GiftWrap,
113            Transport::Nip44Direct => Kind::PrivateDirectMessage,
114        }
115    }
116
117    /// The Mostro protocol version this transport carries, for capability
118    /// advertisement (the `protocol_versions` tag of the node info event).
119    pub fn protocol_version(&self) -> u8 {
120        match self {
121            Transport::GiftWrap => 1,
122            Transport::Nip44Direct => 2,
123        }
124    }
125}
126
127#[allow(deprecated)]
128impl FromStr for Transport {
129    type Err = MostroError;
130
131    fn from_str(s: &str) -> Result<Self, Self::Err> {
132        match s {
133            "gift-wrap" => Ok(Transport::GiftWrap),
134            "nip44" => Ok(Transport::Nip44Direct),
135            other => Err(MostroError::MostroInternalErr(
136                ServiceError::UnexpectedError(format!(
137                    "unknown transport {other:?}; expected \"gift-wrap\" or \"nip44\""
138                )),
139            )),
140        }
141    }
142}
143
144#[allow(deprecated)]
145impl std::fmt::Display for Transport {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        let s = match self {
148            Transport::GiftWrap => "gift-wrap",
149            Transport::Nip44Direct => "nip44",
150        };
151        write!(f, "{s}")
152    }
153}
154
155/// Build a protocol-v2 direct message event (`kind: 14`) ready to publish.
156///
157/// * `message` — the Mostro message to send.
158/// * `identity_keys` — long-lived identity keys. When they differ from
159///   `trade_keys` (reputation mode) the encrypted tuple carries an identity
160///   proof: the identity pubkey plus its [`Message::sign`] signature over
161///   the domain-tagged payload binding the trade pubkey to the message
162///   JSON (see [`identity_proof_payload`]). Pass the same value as
163///   `trade_keys` for full-privacy mode — the proof is then omitted and
164///   the receiver treats the trade key as the identity.
165/// * `trade_keys` — per-trade keys. They author and sign the event (the
166///   visible, rate-limitable sender) and produce the inner tuple signature
167///   when `opts.signed` is `true`.
168/// * `receiver` — the counterparty (Mostro node, or a client's trade key
169///   for node-originated messages). The NIP-44 conversation key is derived
170///   from `trade_keys` and `receiver`, so only those two parties can
171///   decrypt the content.
172/// * `opts` — PoW difficulty, NIP-40 expiration and inner-signature flag,
173///   same semantics as the gift-wrap transport.
174pub fn wrap_message_nip44(
175    message: &Message,
176    identity_keys: &Keys,
177    trade_keys: &Keys,
178    receiver: PublicKey,
179    opts: WrapOptions,
180) -> Result<Event, MostroError> {
181    let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
182
183    let trade_sig = opts
184        .signed
185        .then(|| Message::sign(message_json.clone(), trade_keys).to_string());
186
187    let identity_proof = (identity_keys.public_key() != trade_keys.public_key()).then(|| {
188        let payload = identity_proof_payload(&trade_keys.public_key(), &message_json);
189        (
190            identity_keys.public_key().to_hex(),
191            Message::sign(payload, identity_keys).to_string(),
192        )
193    });
194
195    let tuple: (&Message, Option<String>, Option<(String, String)>) =
196        (message, trade_sig, identity_proof);
197    let content = serde_json::to_string(&tuple)
198        .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?;
199
200    let encrypted = nip44::encrypt(
201        trade_keys.secret_key(),
202        &receiver,
203        content,
204        nip44::Version::default(),
205    )
206    .map_err(|e| MostroError::MostroInternalErr(ServiceError::EncryptionError(e.to_string())))?;
207
208    let mut tags: Vec<Tag> = vec![Tag::public_key(receiver)];
209    if let Some(exp) = opts.expiration {
210        tags.push(Tag::expiration(exp));
211    }
212
213    EventBuilder::new(Kind::PrivateDirectMessage, encrypted)
214        .tags(tags)
215        .pow(opts.pow)
216        .sign_with_keys(trade_keys)
217        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))
218}
219
220/// Try to open an incoming protocol-v2 direct message (`kind: 14`) with the
221/// given `receiver_keys`.
222///
223/// Returns `Ok(None)` only when the NIP-44 content could not be decrypted
224/// with `receiver_keys` — the "not addressed to me" signal, mirroring
225/// [`nip59::unwrap_message`]. Every other failure (invalid event signature,
226/// malformed tuple, non-verifying inner signatures) yields `Err`.
227///
228/// On success, [`UnwrappedMessage::sender`] is the event author (the trade
229/// key) and [`UnwrappedMessage::identity`] is the proven identity pubkey
230/// from the tuple, or the trade key itself when no proof was attached
231/// (full-privacy mode).
232pub fn unwrap_message_nip44(
233    event: &Event,
234    receiver_keys: &Keys,
235) -> Result<Option<UnwrappedMessage>, MostroError> {
236    if event.kind != Kind::PrivateDirectMessage {
237        return Err(MostroError::MostroInternalErr(
238            ServiceError::UnexpectedError("event is not a direct message".to_string()),
239        ));
240    }
241
242    // The event signature is the trade-key authorship proof — unlike the
243    // gift wrap's outer layer (signed by a throwaway ephemeral key), it
244    // must be valid before anything in the content is trusted.
245    event.verify().map_err(|_| {
246        MostroError::MostroInternalErr(ServiceError::NostrError(
247            "invalid event signature".to_string(),
248        ))
249    })?;
250
251    // Decrypt using (receiver_secret, trade_pubkey). Failure here is the
252    // "not addressed to me" signal.
253    let plaintext = match nip44::decrypt(receiver_keys.secret_key(), &event.pubkey, &event.content)
254    {
255        Ok(p) => p,
256        Err(_) => return Ok(None),
257    };
258
259    let (message, trade_sig, identity_proof): DirectTuple = serde_json::from_str(&plaintext)
260        .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?;
261
262    let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
263
264    let signature = match trade_sig {
265        Some(s) => {
266            let sig = Signature::from_str(&s).map_err(|e| {
267                MostroError::MostroInternalErr(ServiceError::UnexpectedError(format!(
268                    "malformed trade signature: {e}"
269                )))
270            })?;
271            if !Message::verify_signature(message_json.clone(), event.pubkey, sig) {
272                return Err(MostroError::MostroInternalErr(
273                    ServiceError::UnexpectedError(
274                        "trade signature does not verify against event author".to_string(),
275                    ),
276                ));
277            }
278            Some(sig)
279        }
280        None => None,
281    };
282
283    let identity = match identity_proof {
284        Some((pubkey, sig)) => {
285            let identity_pubkey = PublicKey::from_str(&pubkey).map_err(|e| {
286                MostroError::MostroInternalErr(ServiceError::UnexpectedError(format!(
287                    "malformed identity pubkey: {e}"
288                )))
289            })?;
290            let identity_sig = Signature::from_str(&sig).map_err(|e| {
291                MostroError::MostroInternalErr(ServiceError::UnexpectedError(format!(
292                    "malformed identity signature: {e}"
293                )))
294            })?;
295            let payload = identity_proof_payload(&event.pubkey, &message_json);
296            if !Message::verify_signature(payload, identity_pubkey, identity_sig) {
297                return Err(MostroError::MostroInternalErr(
298                    ServiceError::UnexpectedError(
299                        "identity signature does not verify against identity pubkey".to_string(),
300                    ),
301                ));
302            }
303            identity_pubkey
304        }
305        None => event.pubkey,
306    };
307
308    Ok(Some(UnwrappedMessage {
309        message,
310        signature,
311        sender: event.pubkey,
312        identity,
313        created_at: event.created_at,
314    }))
315}
316
317/// Wrap `message` for the given `transport`. Thin dispatcher over
318/// [`nip59::wrap_message`] and [`wrap_message_nip44`] so senders hold a
319/// single code path.
320pub async fn wrap_message_with(
321    transport: Transport,
322    message: &Message,
323    identity_keys: &Keys,
324    trade_keys: &Keys,
325    receiver: PublicKey,
326    opts: WrapOptions,
327) -> Result<Event, MostroError> {
328    // Dispatching over the deprecated v1 variant stays supported until the
329    // variant is removed.
330    #[allow(deprecated)]
331    match transport {
332        Transport::GiftWrap => {
333            nip59::wrap_message(message, identity_keys, trade_keys, receiver, opts).await
334        }
335        Transport::Nip44Direct => {
336            wrap_message_nip44(message, identity_keys, trade_keys, receiver, opts)
337        }
338    }
339}
340
341/// Try to open an incoming event on whichever Mostro transport matches its
342/// kind, returning the transport-agnostic [`UnwrappedMessage`].
343///
344/// * `kind: 1059` → the v1 gift-wrap path ([`nip59::unwrap_message`]).
345/// * `kind: 14` → the v2 direct path ([`unwrap_message_nip44`]).
346/// * anything else → `Err` (the caller subscribed to a kind no Mostro
347///   transport speaks).
348///
349/// `Ok(None)` keeps its "not addressed to me" meaning on both paths.
350pub async fn unwrap_incoming(
351    event: &Event,
352    receiver_keys: &Keys,
353) -> Result<Option<UnwrappedMessage>, MostroError> {
354    match event.kind {
355        Kind::GiftWrap => nip59::unwrap_message(event, receiver_keys).await,
356        Kind::PrivateDirectMessage => unwrap_message_nip44(event, receiver_keys),
357        other => Err(MostroError::MostroInternalErr(
358            ServiceError::UnexpectedError(format!("no Mostro transport for event kind {other}")),
359        )),
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::message::{Action, MessageKind, Payload, Peer};
367    use crate::nip59::wrap_message;
368    use uuid::uuid;
369
370    fn sample_order_message(request_id: Option<u64>) -> Message {
371        let peer = Peer::new(
372            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
373            None,
374        );
375        Message::Order(MessageKind::new(
376            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
377            request_id,
378            Some(1),
379            Action::FiatSentOk,
380            Some(Payload::Peer(peer)),
381        ))
382    }
383
384    // Build a kind-14 event with an arbitrary plaintext tuple so tests can
385    // inject payloads `wrap_message_nip44` would never emit.
386    fn wrap_raw_nip44(trade_keys: &Keys, receiver: PublicKey, plaintext: &str) -> Event {
387        let encrypted = nip44::encrypt(
388            trade_keys.secret_key(),
389            &receiver,
390            plaintext,
391            nip44::Version::default(),
392        )
393        .expect("encrypt");
394        EventBuilder::new(Kind::PrivateDirectMessage, encrypted)
395            .tags([Tag::public_key(receiver)])
396            .sign_with_keys(trade_keys)
397            .expect("sign")
398    }
399
400    #[test]
401    fn nip44_roundtrip_reputation_mode() {
402        let identity_keys = Keys::generate();
403        let trade_keys = Keys::generate();
404        let receiver_keys = Keys::generate();
405        let message = sample_order_message(Some(42));
406
407        let event = wrap_message_nip44(
408            &message,
409            &identity_keys,
410            &trade_keys,
411            receiver_keys.public_key(),
412            WrapOptions::default(),
413        )
414        .expect("wrap");
415
416        assert_eq!(event.kind, Kind::PrivateDirectMessage);
417        assert_eq!(event.pubkey, trade_keys.public_key());
418        assert!(event
419            .tags
420            .iter()
421            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("p")));
422
423        let unwrapped = unwrap_message_nip44(&event, &receiver_keys)
424            .expect("unwrap result")
425            .expect("unwrap some");
426
427        assert_eq!(unwrapped.sender, trade_keys.public_key());
428        assert_eq!(unwrapped.identity, identity_keys.public_key());
429        assert!(unwrapped.signature.is_some());
430        assert_eq!(
431            unwrapped.message.as_json().unwrap(),
432            message.as_json().unwrap()
433        );
434    }
435
436    #[test]
437    fn nip44_full_privacy_identity_equals_sender() {
438        let trade_keys = Keys::generate();
439        let receiver_keys = Keys::generate();
440
441        let event = wrap_message_nip44(
442            &sample_order_message(Some(1)),
443            &trade_keys,
444            &trade_keys,
445            receiver_keys.public_key(),
446            WrapOptions {
447                signed: false,
448                ..WrapOptions::default()
449            },
450        )
451        .expect("wrap");
452
453        let unwrapped = unwrap_message_nip44(&event, &receiver_keys)
454            .expect("unwrap")
455            .expect("some");
456
457        assert_eq!(unwrapped.sender, trade_keys.public_key());
458        assert_eq!(unwrapped.identity, trade_keys.public_key());
459        assert!(unwrapped.signature.is_none());
460    }
461
462    #[test]
463    fn nip44_messages_are_stamped_protocol_v2() {
464        let trade_keys = Keys::generate();
465        let receiver_keys = Keys::generate();
466
467        let event = wrap_message_nip44(
468            &sample_order_message(Some(1)),
469            &trade_keys,
470            &trade_keys,
471            receiver_keys.public_key(),
472            WrapOptions::default(),
473        )
474        .expect("wrap");
475
476        let unwrapped = unwrap_message_nip44(&event, &receiver_keys)
477            .expect("unwrap")
478            .expect("some");
479        assert_eq!(unwrapped.message.get_inner_message_kind().version, 2);
480    }
481
482    #[test]
483    fn nip44_wrong_receiver_returns_none() {
484        let trade_keys = Keys::generate();
485        let receiver_keys = Keys::generate();
486        let stranger_keys = Keys::generate();
487
488        let event = wrap_message_nip44(
489            &sample_order_message(Some(1)),
490            &trade_keys,
491            &trade_keys,
492            receiver_keys.public_key(),
493            WrapOptions::default(),
494        )
495        .expect("wrap");
496
497        let result = unwrap_message_nip44(&event, &stranger_keys).expect("call should not error");
498        assert!(result.is_none());
499    }
500
501    #[test]
502    fn nip44_forged_identity_proof_errors() {
503        let identity_keys = Keys::generate();
504        let trade_keys = Keys::generate();
505        let receiver_keys = Keys::generate();
506        let message = sample_order_message(Some(1));
507
508        // Identity signature over a different payload must be rejected.
509        let bogus_sig = Message::sign("not the real message".to_string(), &identity_keys);
510        let tuple: (&Message, Option<String>, Option<(String, String)>) = (
511            &message,
512            None,
513            Some((identity_keys.public_key().to_hex(), bogus_sig.to_string())),
514        );
515        let plaintext = serde_json::to_string(&tuple).unwrap();
516        let event = wrap_raw_nip44(&trade_keys, receiver_keys.public_key(), &plaintext);
517
518        let result = unwrap_message_nip44(&event, &receiver_keys);
519        assert!(
520            matches!(result, Err(MostroError::MostroInternalErr(_))),
521            "forged identity proof must surface as Err, got {result:?}",
522        );
523    }
524
525    #[test]
526    fn nip44_identity_proof_grafted_onto_other_trade_key_errors() {
527        // A valid identity proof produced for trade key T must not verify
528        // when replayed inside an event authored by another trade key T',
529        // even with the identical Message — the proof payload includes the
530        // trade pubkey precisely to prevent this grafting.
531        let identity_keys = Keys::generate();
532        let trade_keys = Keys::generate();
533        let attacker_trade_keys = Keys::generate();
534        let receiver_keys = Keys::generate();
535        let message = sample_order_message(Some(1));
536        let message_json = message.as_json().unwrap();
537
538        // Legitimate proof, bound to the victim's trade key.
539        let payload = identity_proof_payload(&trade_keys.public_key(), &message_json);
540        let legit_sig = Message::sign(payload, &identity_keys);
541
542        // Attacker replays it under their own trade key.
543        let tuple: (&Message, Option<String>, Option<(String, String)>) = (
544            &message,
545            None,
546            Some((identity_keys.public_key().to_hex(), legit_sig.to_string())),
547        );
548        let plaintext = serde_json::to_string(&tuple).unwrap();
549        let event = wrap_raw_nip44(&attacker_trade_keys, receiver_keys.public_key(), &plaintext);
550
551        let result = unwrap_message_nip44(&event, &receiver_keys);
552        assert!(
553            matches!(result, Err(MostroError::MostroInternalErr(_))),
554            "grafted identity proof must surface as Err, got {result:?}",
555        );
556
557        // Sanity check: the same proof under the right trade key verifies.
558        let event = wrap_raw_nip44(&trade_keys, receiver_keys.public_key(), &plaintext);
559        let unwrapped = unwrap_message_nip44(&event, &receiver_keys)
560            .expect("unwrap")
561            .expect("some");
562        assert_eq!(unwrapped.identity, identity_keys.public_key());
563    }
564
565    #[test]
566    fn nip44_trade_sig_from_other_key_errors() {
567        let trade_keys = Keys::generate();
568        let other_keys = Keys::generate();
569        let receiver_keys = Keys::generate();
570        let message = sample_order_message(Some(1));
571        let message_json = message.as_json().unwrap();
572
573        // Inner signature produced by a key other than the event author.
574        let foreign_sig = Message::sign(message_json, &other_keys);
575        let tuple: (&Message, Option<String>, Option<(String, String)>) =
576            (&message, Some(foreign_sig.to_string()), None);
577        let plaintext = serde_json::to_string(&tuple).unwrap();
578        let event = wrap_raw_nip44(&trade_keys, receiver_keys.public_key(), &plaintext);
579
580        let result = unwrap_message_nip44(&event, &receiver_keys);
581        assert!(
582            matches!(result, Err(MostroError::MostroInternalErr(_))),
583            "foreign trade signature must surface as Err, got {result:?}",
584        );
585    }
586
587    #[test]
588    fn nip44_malformed_tuple_errors() {
589        let trade_keys = Keys::generate();
590        let receiver_keys = Keys::generate();
591
592        // Decrypts fine but is not a valid 3-element tuple.
593        let event = wrap_raw_nip44(&trade_keys, receiver_keys.public_key(), "not a tuple");
594
595        let result = unwrap_message_nip44(&event, &receiver_keys);
596        assert!(
597            matches!(result, Err(MostroError::MostroInternalErr(_))),
598            "malformed tuple must surface as Err, got {result:?}",
599        );
600    }
601
602    #[test]
603    fn nip44_expiration_tag_is_set_when_provided() {
604        let trade_keys = Keys::generate();
605        let receiver_keys = Keys::generate();
606        let exp = Timestamp::from_secs(Timestamp::now().as_secs() + 3600);
607
608        let event = wrap_message_nip44(
609            &sample_order_message(Some(1)),
610            &trade_keys,
611            &trade_keys,
612            receiver_keys.public_key(),
613            WrapOptions {
614                expiration: Some(exp),
615                ..WrapOptions::default()
616            },
617        )
618        .expect("wrap");
619
620        let has_expiration = event
621            .tags
622            .iter()
623            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("expiration"));
624        assert!(has_expiration);
625    }
626
627    #[test]
628    fn nip44_pow_is_applied() {
629        let trade_keys = Keys::generate();
630        let receiver_keys = Keys::generate();
631
632        let event = wrap_message_nip44(
633            &sample_order_message(Some(1)),
634            &trade_keys,
635            &trade_keys,
636            receiver_keys.public_key(),
637            WrapOptions {
638                pow: 8,
639                ..WrapOptions::default()
640            },
641        )
642        .expect("wrap");
643
644        assert!(event.check_pow(8));
645    }
646
647    #[tokio::test]
648    async fn unwrap_incoming_dispatches_both_transports() {
649        let identity_keys = Keys::generate();
650        let trade_keys = Keys::generate();
651        let receiver_keys = Keys::generate();
652        let message = sample_order_message(Some(7));
653
654        let wrapped = wrap_message(
655            &message,
656            &identity_keys,
657            &trade_keys,
658            receiver_keys.public_key(),
659            WrapOptions::default(),
660        )
661        .await
662        .expect("gift wrap");
663        let direct = wrap_message_nip44(
664            &message,
665            &identity_keys,
666            &trade_keys,
667            receiver_keys.public_key(),
668            WrapOptions::default(),
669        )
670        .expect("nip44 wrap");
671
672        for event in [wrapped, direct] {
673            let unwrapped = unwrap_incoming(&event, &receiver_keys)
674                .await
675                .expect("unwrap")
676                .expect("some");
677            assert_eq!(unwrapped.sender, trade_keys.public_key());
678            assert_eq!(unwrapped.identity, identity_keys.public_key());
679            assert_eq!(
680                unwrapped.message.as_json().unwrap(),
681                message.as_json().unwrap()
682            );
683        }
684    }
685
686    #[tokio::test]
687    async fn unwrap_incoming_rejects_unknown_kind() {
688        let keys = Keys::generate();
689        let event = EventBuilder::text_note("hello")
690            .sign_with_keys(&keys)
691            .expect("sign");
692
693        let result = unwrap_incoming(&event, &keys).await;
694        assert!(matches!(result, Err(MostroError::MostroInternalErr(_))));
695    }
696
697    #[tokio::test]
698    // Exercises the deprecated v1 variant on purpose — coverage must hold
699    // until the variant is removed.
700    #[allow(deprecated)]
701    async fn wrap_message_with_dispatches_by_transport() {
702        let trade_keys = Keys::generate();
703        let receiver_keys = Keys::generate();
704        let message = sample_order_message(Some(1));
705
706        let gift = wrap_message_with(
707            Transport::GiftWrap,
708            &message,
709            &trade_keys,
710            &trade_keys,
711            receiver_keys.public_key(),
712            WrapOptions::default(),
713        )
714        .await
715        .expect("gift wrap");
716        let direct = wrap_message_with(
717            Transport::Nip44Direct,
718            &message,
719            &trade_keys,
720            &trade_keys,
721            receiver_keys.public_key(),
722            WrapOptions::default(),
723        )
724        .await
725        .expect("nip44");
726
727        assert_eq!(gift.kind, Kind::GiftWrap);
728        assert_eq!(direct.kind, Kind::PrivateDirectMessage);
729    }
730
731    #[test]
732    // Exercises the deprecated v1 variant on purpose — coverage must hold
733    // until the variant is removed.
734    #[allow(deprecated)]
735    fn transport_config_parsing() {
736        for (s, expected) in [
737            ("gift-wrap", Transport::GiftWrap),
738            ("nip44", Transport::Nip44Direct),
739        ] {
740            assert_eq!(s.parse::<Transport>().unwrap(), expected);
741            let from_serde: Transport = serde_json::from_str(&format!("{s:?}")).unwrap();
742            assert_eq!(from_serde, expected);
743            assert_eq!(expected.to_string(), s);
744        }
745        assert!("dual".parse::<Transport>().is_err());
746        assert!("bogus".parse::<Transport>().is_err());
747        assert_eq!(Transport::default(), Transport::GiftWrap);
748    }
749
750    #[test]
751    // Exercises the deprecated v1 variant on purpose — coverage must hold
752    // until the variant is removed.
753    #[allow(deprecated)]
754    fn transport_kind_and_version() {
755        assert_eq!(Transport::GiftWrap.event_kind(), Kind::GiftWrap);
756        assert_eq!(
757            Transport::Nip44Direct.event_kind(),
758            Kind::PrivateDirectMessage
759        );
760        assert_eq!(Transport::GiftWrap.protocol_version(), 1);
761        assert_eq!(Transport::Nip44Direct.protocol_version(), 2);
762    }
763}