Skip to main content

mostro_core/
nip59.rs

1//! NIP-59 GiftWrap transport for Mostro messages.
2//!
3//! Every message exchanged with a Mostro node travels through the same
4//! pipeline:
5//!
6//! ```text
7//! Message -> JSON((Message, Option<Signature>)) -> Rumor -> Seal -> GiftWrap
8//! ```
9//!
10//! Mostro splits signing across two keys: a long-lived **identity key**
11//! signs the seal (and encrypts it to the receiver), while a per-trade
12//! **trade key** authors the rumor and produces the inner tuple signature.
13//! This deliberately breaks NIP-59's "rumor author == seal signer"
14//! convention that `nostr-sdk` 0.44 enforces via `SenderMismatch`, so the
15//! unwrap path does its own NIP-44 + signature verification instead of
16//! calling `nip59::extract_rumor`.
17//!
18//! The module centralizes wrap/unwrap so clients do not need to reimplement
19//! NIP-59 glue themselves. It does not manage relays, subscriptions,
20//! waiters or persistence — the returned `Event` is ready to publish, and
21//! the caller decides how to do so.
22
23use std::str::FromStr;
24
25use crate::message::{Action, Message, Payload};
26use crate::prelude::{CantDoReason, MostroError, ServiceError};
27use nostr_sdk::nips::{nip44, nip59};
28use nostr_sdk::prelude::*;
29
30/// Options controlling how a Mostro message is wrapped.
31#[derive(Debug, Clone)]
32pub struct WrapOptions {
33    /// NIP-13 proof-of-work difficulty applied to the outer GiftWrap event.
34    pub pow: u8,
35    /// Optional expiration tag for the outer GiftWrap event.
36    pub expiration: Option<Timestamp>,
37    /// When true the inner rumor content is `(Message, Some(Signature))`,
38    /// with the signature produced from the JSON of `Message` using
39    /// `trade_keys`. When false the content is `(Message, None)`. Traffic
40    /// to a Mostro node always uses `true`.
41    pub signed: bool,
42}
43
44impl Default for WrapOptions {
45    fn default() -> Self {
46        Self {
47            pow: 0,
48            expiration: None,
49            signed: true,
50        }
51    }
52}
53
54/// A Mostro message recovered from an incoming GiftWrap, plus metadata from
55/// the outer envelopes.
56#[derive(Debug, Clone)]
57pub struct UnwrappedMessage {
58    /// The logical Mostro message carried inside the rumor.
59    pub message: Message,
60    /// Signature of the JSON-serialized `Message`, produced with the sender's
61    /// trade keys. Present only when the sender set `signed = true`.
62    pub signature: Option<Signature>,
63    /// Rumor author — the sender's trade public key.
64    pub sender: PublicKey,
65    /// Seal signer — the sender's long-lived identity public key. In
66    /// full-privacy mode (where the client reuses its trade key as identity)
67    /// this equals `sender`.
68    pub identity: PublicKey,
69    /// Rumor `created_at` timestamp.
70    pub created_at: Timestamp,
71}
72
73/// Build a GiftWrap event (`kind: 1059`) ready to be published to a relay.
74///
75/// * `message` — the Mostro message to send.
76/// * `identity_keys` — long-lived identity keys. Sign the seal (kind 13)
77///   and encrypt it to `receiver` via NIP-44. Callers that want the
78///   "full privacy" mode (no stable identity, no reputation) should pass
79///   the same value as `trade_keys`.
80/// * `trade_keys` — per-trade keys. Author of the rumor (kind 1) and
81///   signer of the inner tuple signature when `opts.signed == true`.
82/// * `receiver` — the Mostro node public key.
83/// * `opts` — wrap options (PoW, expiration, signed).
84pub async fn wrap_message(
85    message: &Message,
86    identity_keys: &Keys,
87    trade_keys: &Keys,
88    receiver: PublicKey,
89    opts: WrapOptions,
90) -> Result<Event, MostroError> {
91    let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
92
93    let content = if opts.signed {
94        let sig = Message::sign(message_json, trade_keys);
95        serde_json::to_string(&(message, Some(sig.to_string())))
96            .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?
97    } else {
98        serde_json::to_string(&(message, Option::<String>::None))
99            .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?
100    };
101
102    // PoW only applies to the outer GiftWrap (per WrapOptions docs); the
103    // rumor is encrypted inside the seal and never published on its own,
104    // so mining its event id would burn CPU for nothing.
105    let rumor = EventBuilder::text_note(content).build(trade_keys.public_key());
106
107    // Seal is encrypted and signed with identity_keys so the receiver can
108    // decrypt it via (receiver_secret, seal.pubkey) — this keeps seal.pubkey
109    // consistent with the encryption key, while leaving rumor.pubkey free to
110    // carry the per-trade key (the mismatch standard NIP-59 rejects).
111    let seal: Event = EventBuilder::seal(identity_keys, &receiver, rumor)
112        .await
113        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?
114        .sign(identity_keys)
115        .await
116        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
117
118    gift_wrap_from_seal_with_pow(&seal, receiver, opts.pow, opts.expiration)
119}
120
121/// Wrap an already built Seal into a NIP-59 GiftWrap with optional PoW and
122/// expiration. The outer event is signed with a freshly generated ephemeral
123/// key and carries a mandatory `p` tag pointing at `receiver`.
124fn gift_wrap_from_seal_with_pow(
125    seal: &Event,
126    receiver: PublicKey,
127    pow: u8,
128    expiration: Option<Timestamp>,
129) -> Result<Event, MostroError> {
130    if seal.kind != Kind::Seal {
131        return Err(MostroError::MostroInternalErr(
132            ServiceError::UnexpectedError("expected Seal kind".to_string()),
133        ));
134    }
135
136    let ephemeral = Keys::generate();
137    let encrypted = nip44::encrypt(
138        ephemeral.secret_key(),
139        &receiver,
140        seal.as_json(),
141        nip44::Version::default(),
142    )
143    .map_err(|e| MostroError::MostroInternalErr(ServiceError::EncryptionError(e.to_string())))?;
144
145    let mut tags: Vec<Tag> = Vec::new();
146    if let Some(exp) = expiration {
147        tags.push(Tag::expiration(exp));
148    }
149    tags.push(Tag::public_key(receiver));
150
151    EventBuilder::new(Kind::GiftWrap, encrypted)
152        .tags(tags)
153        .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK))
154        .pow(pow)
155        .sign_with_keys(&ephemeral)
156        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))
157}
158
159/// Try to open an incoming GiftWrap with the given `receiver_keys`.
160///
161/// Returns `Ok(None)` only when the outer NIP-44 layer could not be
162/// decrypted with `receiver_keys` — the canonical "not addressed to me"
163/// signal, so callers can try multiple candidate keys without treating
164/// each miss as fatal. Every other failure (corrupted seal, malformed
165/// rumor JSON, invalid signatures, etc.) yields `Err` so callers can tell
166/// "not mine" apart from "broken".
167///
168/// Does **not** enforce `seal.pubkey == rumor.pubkey`: Mostro signs the
169/// seal with the identity key and authors the rumor with the per-trade
170/// key, so the two legitimately differ.
171pub async fn unwrap_message(
172    event: &Event,
173    receiver_keys: &Keys,
174) -> Result<Option<UnwrappedMessage>, MostroError> {
175    if event.kind != Kind::GiftWrap {
176        return Err(MostroError::MostroInternalErr(
177            ServiceError::UnexpectedError("event is not a GiftWrap".to_string()),
178        ));
179    }
180
181    // Decrypt outer GiftWrap using (receiver_secret, ephemeral_pub).
182    // Failure here is the "not addressed to me" signal.
183    let seal_json = match nip44::decrypt(receiver_keys.secret_key(), &event.pubkey, &event.content)
184    {
185        Ok(s) => s,
186        Err(_) => return Ok(None),
187    };
188
189    let seal: Event = Event::from_json(&seal_json).map_err(|e| {
190        MostroError::MostroInternalErr(ServiceError::NostrError(format!(
191            "malformed seal JSON: {e}"
192        )))
193    })?;
194
195    if seal.kind != Kind::Seal {
196        return Err(MostroError::MostroInternalErr(
197            ServiceError::UnexpectedError("inner event is not a Seal".to_string()),
198        ));
199    }
200
201    seal.verify_signature().then_some(()).ok_or_else(|| {
202        MostroError::MostroInternalErr(ServiceError::NostrError(
203            "invalid seal signature".to_string(),
204        ))
205    })?;
206
207    // Decrypt the seal content using (receiver_secret, seal.pubkey). In
208    // Mostro, seal.pubkey is the sender's identity key, which is also the
209    // key that performed the NIP-44 encryption in `wrap_message`.
210    let rumor_json = nip44::decrypt(receiver_keys.secret_key(), &seal.pubkey, &seal.content)
211        .map_err(|e| {
212            MostroError::MostroInternalErr(ServiceError::DecryptionError(e.to_string()))
213        })?;
214
215    let rumor: UnsignedEvent = UnsignedEvent::from_json(&rumor_json).map_err(|e| {
216        MostroError::MostroInternalErr(ServiceError::NostrError(format!(
217            "malformed rumor JSON: {e}"
218        )))
219    })?;
220
221    if rumor.kind != Kind::TextNote {
222        return Err(MostroError::MostroInternalErr(
223            ServiceError::UnexpectedError("rumor is not a TextNote".to_string()),
224        ));
225    }
226
227    let (message, sig_str): (Message, Option<String>) = serde_json::from_str(&rumor.content)
228        .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?;
229
230    let signature = match sig_str {
231        Some(s) => {
232            let sig = Signature::from_str(&s).map_err(|e| {
233                MostroError::MostroInternalErr(ServiceError::UnexpectedError(format!(
234                    "malformed rumor signature: {e}"
235                )))
236            })?;
237            let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
238            if !Message::verify_signature(message_json, rumor.pubkey, sig) {
239                return Err(MostroError::MostroInternalErr(
240                    ServiceError::UnexpectedError(
241                        "rumor signature does not verify against sender".to_string(),
242                    ),
243                ));
244            }
245            Some(sig)
246        }
247        None => None,
248    };
249
250    Ok(Some(UnwrappedMessage {
251        message,
252        signature,
253        sender: rumor.pubkey,
254        identity: seal.pubkey,
255        created_at: rumor.created_at,
256    }))
257}
258
259/// Validate a response received from a Mostro node.
260///
261/// * Returns `Err(MostroCantDo(reason))` when the payload is `CantDo`.
262/// * Returns `Err(MostroInternalErr(...))` when `expected_request_id` is
263///   provided and the inner message carries a different id, or no id at all
264///   on an action that requires one.
265/// * Otherwise returns `Ok(())`.
266///
267/// The allow-list of actions that may arrive without a `request_id` (server
268/// push messages such as state transitions, DMs, payment failures, etc.) is
269/// intentionally kept on the caller side, because the exact set depends on
270/// the client flow; this function only enforces the universal rules.
271pub fn validate_response(
272    message: &Message,
273    expected_request_id: Option<u64>,
274) -> Result<(), MostroError> {
275    let inner = message.get_inner_message_kind();
276
277    if let Some(Payload::CantDo(reason)) = &inner.payload {
278        return Err(MostroError::MostroCantDo(
279            reason.clone().unwrap_or(CantDoReason::InvalidAction),
280        ));
281    }
282
283    if let Some(expected) = expected_request_id {
284        match inner.request_id {
285            Some(got) if got == expected => {}
286            Some(_) => {
287                return Err(MostroError::MostroInternalErr(
288                    ServiceError::UnexpectedError("mismatched request_id".to_string()),
289                ));
290            }
291            None => {
292                if !action_accepts_missing_request_id(&inner.action) {
293                    return Err(MostroError::MostroInternalErr(
294                        ServiceError::UnexpectedError(
295                            "missing request_id on a response that requires one".to_string(),
296                        ),
297                    ));
298                }
299            }
300        }
301    }
302
303    Ok(())
304}
305
306/// Actions that may legitimately arrive without a `request_id` even when the
307/// caller was waiting on one (unsolicited server-initiated events).
308fn action_accepts_missing_request_id(action: &Action) -> bool {
309    matches!(
310        action,
311        Action::BuyerTookOrder
312            | Action::HoldInvoicePaymentAccepted
313            | Action::HoldInvoicePaymentSettled
314            | Action::HoldInvoicePaymentCanceled
315            | Action::WaitingSellerToPay
316            | Action::WaitingBuyerInvoice
317            | Action::BuyerInvoiceAccepted
318            | Action::PurchaseCompleted
319            | Action::Released
320            | Action::FiatSentOk
321            | Action::Canceled
322            | Action::CooperativeCancelInitiatedByPeer
323            | Action::CooperativeCancelAccepted
324            | Action::DisputeInitiatedByPeer
325            | Action::AdminSettled
326            | Action::AdminCanceled
327            | Action::AdminTookDispute
328            | Action::PaymentFailed
329            | Action::InvoiceUpdated
330            | Action::Rate
331            | Action::RateReceived
332            | Action::SendDm
333            | Action::BondSlashed
334            | Action::CashuEscrowLocked
335            | Action::CashuPmSignature
336    )
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::message::{Action, MessageKind, Payload};
343    use uuid::uuid;
344
345    fn sample_order_message(request_id: Option<u64>) -> Message {
346        let peer = crate::message::Peer::new(
347            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
348            None,
349        );
350        Message::Order(MessageKind::new(
351            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
352            request_id,
353            Some(1),
354            Action::FiatSentOk,
355            Some(Payload::Peer(peer)),
356        ))
357    }
358
359    #[tokio::test]
360    async fn wrap_then_unwrap_roundtrip() {
361        let identity_keys = Keys::generate();
362        let trade_keys = Keys::generate();
363        let receiver_keys = Keys::generate();
364
365        let message = sample_order_message(Some(42));
366
367        let wrapped = wrap_message(
368            &message,
369            &identity_keys,
370            &trade_keys,
371            receiver_keys.public_key(),
372            WrapOptions::default(),
373        )
374        .await
375        .expect("wrap");
376
377        assert_eq!(wrapped.kind, Kind::GiftWrap);
378        assert!(wrapped
379            .tags
380            .iter()
381            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("p")));
382
383        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
384            .await
385            .expect("unwrap result")
386            .expect("unwrap some");
387
388        assert_eq!(unwrapped.sender, trade_keys.public_key());
389        assert_eq!(unwrapped.identity, identity_keys.public_key());
390        assert_eq!(
391            unwrapped.message.as_json().unwrap(),
392            message.as_json().unwrap()
393        );
394        assert!(unwrapped.signature.is_some());
395    }
396
397    #[tokio::test]
398    async fn full_privacy_mode_identity_equals_sender() {
399        // Caller that opts out of reputation passes trade_keys as identity.
400        let trade_keys = Keys::generate();
401        let receiver_keys = Keys::generate();
402
403        let wrapped = wrap_message(
404            &sample_order_message(Some(1)),
405            &trade_keys,
406            &trade_keys,
407            receiver_keys.public_key(),
408            WrapOptions::default(),
409        )
410        .await
411        .expect("wrap");
412
413        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
414            .await
415            .expect("unwrap")
416            .expect("some");
417
418        assert_eq!(unwrapped.sender, trade_keys.public_key());
419        assert_eq!(unwrapped.identity, trade_keys.public_key());
420    }
421
422    #[tokio::test]
423    async fn unwrap_with_corrupted_seal_returns_err() {
424        let receiver_keys = Keys::generate();
425        let ephemeral = Keys::generate();
426
427        // GiftWrap whose outer ciphertext decrypts successfully but yields
428        // a string that is not a valid seal Event JSON. Must surface as
429        // Err, not be silently absorbed as Ok(None).
430        let encrypted = nip44::encrypt(
431            ephemeral.secret_key(),
432            &receiver_keys.public_key(),
433            "not a seal",
434            nip44::Version::default(),
435        )
436        .expect("encrypt");
437
438        let corrupted = EventBuilder::new(Kind::GiftWrap, encrypted)
439            .tags([Tag::public_key(receiver_keys.public_key())])
440            .sign_with_keys(&ephemeral)
441            .expect("sign");
442
443        let result = unwrap_message(&corrupted, &receiver_keys).await;
444        assert!(
445            matches!(result, Err(MostroError::MostroInternalErr(_))),
446            "expected Err for corrupted gift wrap, got {result:?}",
447        );
448    }
449
450    // Build a GiftWrap by hand with a custom inner rumor tuple so tests
451    // can inject a malformed or wrong-signature payload that `wrap_message`
452    // would never emit.
453    async fn wrap_with_raw_inner(
454        identity_keys: &Keys,
455        trade_keys: &Keys,
456        receiver: PublicKey,
457        inner: (&Message, Option<String>),
458    ) -> Event {
459        let content = serde_json::to_string(&inner).unwrap();
460        let rumor = EventBuilder::text_note(content).build(trade_keys.public_key());
461        let seal = EventBuilder::seal(identity_keys, &receiver, rumor)
462            .await
463            .unwrap()
464            .sign(identity_keys)
465            .await
466            .unwrap();
467        gift_wrap_from_seal_with_pow(&seal, receiver, 0, None).unwrap()
468    }
469
470    #[tokio::test]
471    async fn unwrap_with_malformed_signature_errors() {
472        let identity_keys = Keys::generate();
473        let trade_keys = Keys::generate();
474        let receiver_keys = Keys::generate();
475        let msg = sample_order_message(Some(1));
476
477        let wrapped = wrap_with_raw_inner(
478            &identity_keys,
479            &trade_keys,
480            receiver_keys.public_key(),
481            (&msg, Some("not-a-hex-signature".to_string())),
482        )
483        .await;
484
485        let result = unwrap_message(&wrapped, &receiver_keys).await;
486        assert!(
487            matches!(result, Err(MostroError::MostroInternalErr(_))),
488            "malformed signature must surface as Err, got {result:?}",
489        );
490    }
491
492    #[tokio::test]
493    async fn unwrap_with_signature_for_other_content_errors() {
494        let identity_keys = Keys::generate();
495        let trade_keys = Keys::generate();
496        let receiver_keys = Keys::generate();
497        let msg = sample_order_message(Some(1));
498        // Well-formed signature, but over a completely different payload.
499        let bogus = Message::sign("not the real message".to_string(), &trade_keys);
500
501        let wrapped = wrap_with_raw_inner(
502            &identity_keys,
503            &trade_keys,
504            receiver_keys.public_key(),
505            (&msg, Some(bogus.to_string())),
506        )
507        .await;
508
509        let result = unwrap_message(&wrapped, &receiver_keys).await;
510        assert!(
511            matches!(result, Err(MostroError::MostroInternalErr(_))),
512            "non-verifying signature must surface as Err, got {result:?}",
513        );
514    }
515
516    #[tokio::test]
517    async fn unwrap_with_wrong_receiver_keys_returns_none() {
518        let identity_keys = Keys::generate();
519        let trade_keys = Keys::generate();
520        let receiver_keys = Keys::generate();
521        let stranger_keys = Keys::generate();
522
523        let wrapped = wrap_message(
524            &sample_order_message(Some(1)),
525            &identity_keys,
526            &trade_keys,
527            receiver_keys.public_key(),
528            WrapOptions::default(),
529        )
530        .await
531        .expect("wrap");
532
533        let result = unwrap_message(&wrapped, &stranger_keys)
534            .await
535            .expect("call should not error");
536        assert!(result.is_none());
537    }
538
539    #[tokio::test]
540    async fn signature_is_verifiable_with_trade_pubkey() {
541        let identity_keys = Keys::generate();
542        let trade_keys = Keys::generate();
543        let receiver_keys = Keys::generate();
544        let message = sample_order_message(Some(7));
545
546        let wrapped = wrap_message(
547            &message,
548            &identity_keys,
549            &trade_keys,
550            receiver_keys.public_key(),
551            WrapOptions::default(),
552        )
553        .await
554        .unwrap();
555
556        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
557            .await
558            .unwrap()
559            .unwrap();
560
561        let sig = unwrapped.signature.expect("signed");
562        let json = unwrapped.message.as_json().unwrap();
563        assert!(Message::verify_signature(
564            json,
565            trade_keys.public_key(),
566            sig
567        ));
568    }
569
570    #[tokio::test]
571    async fn unsigned_wrap_has_no_signature() {
572        let identity_keys = Keys::generate();
573        let trade_keys = Keys::generate();
574        let receiver_keys = Keys::generate();
575
576        let wrapped = wrap_message(
577            &sample_order_message(Some(3)),
578            &identity_keys,
579            &trade_keys,
580            receiver_keys.public_key(),
581            WrapOptions {
582                signed: false,
583                ..WrapOptions::default()
584            },
585        )
586        .await
587        .expect("wrap");
588
589        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
590            .await
591            .unwrap()
592            .unwrap();
593        assert!(unwrapped.signature.is_none());
594    }
595
596    #[tokio::test]
597    async fn expiration_tag_is_set_when_provided() {
598        let identity_keys = Keys::generate();
599        let trade_keys = Keys::generate();
600        let receiver_keys = Keys::generate();
601        let exp = Timestamp::from_secs(Timestamp::now().as_secs() + 3600);
602
603        let wrapped = wrap_message(
604            &sample_order_message(Some(1)),
605            &identity_keys,
606            &trade_keys,
607            receiver_keys.public_key(),
608            WrapOptions {
609                expiration: Some(exp),
610                ..WrapOptions::default()
611            },
612        )
613        .await
614        .expect("wrap");
615
616        let has_expiration = wrapped
617            .tags
618            .iter()
619            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("expiration"));
620        assert!(has_expiration);
621    }
622
623    #[test]
624    fn validate_response_cant_do_short_circuits() {
625        let msg = Message::cant_do(
626            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
627            Some(5),
628            Some(Payload::CantDo(Some(CantDoReason::NotAuthorized))),
629        );
630        let err = validate_response(&msg, Some(5)).unwrap_err();
631        match err {
632            MostroError::MostroCantDo(CantDoReason::NotAuthorized) => {}
633            _ => panic!("expected CantDo(NotAuthorized)"),
634        }
635    }
636
637    #[test]
638    fn validate_response_request_id_match() {
639        let msg = sample_order_message(Some(9));
640        validate_response(&msg, Some(9)).unwrap();
641    }
642
643    #[test]
644    fn validate_response_request_id_mismatch_errors() {
645        let msg = sample_order_message(Some(9));
646        let err = validate_response(&msg, Some(10)).unwrap_err();
647        assert!(matches!(err, MostroError::MostroInternalErr(_)));
648    }
649
650    #[test]
651    fn validate_response_allows_unsolicited_actions_without_request_id() {
652        let msg = Message::Order(MessageKind::new(
653            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
654            None,
655            None,
656            Action::BuyerTookOrder,
657            None,
658        ));
659        validate_response(&msg, Some(1)).unwrap();
660    }
661
662    #[test]
663    fn validate_response_with_no_expected_id_is_ok() {
664        let msg = sample_order_message(None);
665        validate_response(&msg, None).unwrap();
666    }
667
668    #[test]
669    fn validate_response_allows_cashu_server_events_without_request_id() {
670        // Both Cashu notifications are server-originated and may arrive while
671        // the client is still waiting on an earlier request_id.
672        for action in [Action::CashuEscrowLocked, Action::CashuPmSignature] {
673            let msg = Message::Order(MessageKind::new(
674                Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
675                None,
676                None,
677                action,
678                None,
679            ));
680            validate_response(&msg, Some(1)).unwrap();
681        }
682    }
683}