mostro_core/
message.rs

1use crate::prelude::*;
2use bitcoin::hashes::sha256::Hash as Sha256Hash;
3use bitcoin::hashes::Hash;
4use bitcoin::key::Secp256k1;
5use bitcoin::secp256k1::Message as BitcoinMessage;
6use nostr_sdk::prelude::*;
7#[cfg(feature = "sqlx")]
8use sqlx::FromRow;
9#[cfg(feature = "sqlx")]
10use sqlx_crud::SqlxCrud;
11
12use std::fmt;
13use uuid::Uuid;
14
15/// One party of the trade
16#[derive(Debug, Deserialize, Serialize, Clone)]
17pub struct Peer {
18    pub pubkey: String,
19    pub reputation: Option<UserInfo>,
20}
21
22impl Peer {
23    pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
24        Self { pubkey, reputation }
25    }
26
27    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
28        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
29    }
30
31    pub fn as_json(&self) -> Result<String, ServiceError> {
32        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
33    }
34}
35
36/// Action is used to identify each message between Mostro and users
37#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
38#[serde(rename_all = "kebab-case")]
39pub enum Action {
40    NewOrder,
41    TakeSell,
42    TakeBuy,
43    PayInvoice,
44    FiatSent,
45    FiatSentOk,
46    Release,
47    Released,
48    Cancel,
49    Canceled,
50    CooperativeCancelInitiatedByYou,
51    CooperativeCancelInitiatedByPeer,
52    DisputeInitiatedByYou,
53    DisputeInitiatedByPeer,
54    CooperativeCancelAccepted,
55    BuyerInvoiceAccepted,
56    PurchaseCompleted,
57    HoldInvoicePaymentAccepted,
58    HoldInvoicePaymentSettled,
59    HoldInvoicePaymentCanceled,
60    WaitingSellerToPay,
61    WaitingBuyerInvoice,
62    AddInvoice,
63    BuyerTookOrder,
64    Rate,
65    RateUser,
66    RateReceived,
67    CantDo,
68    Dispute,
69    AdminCancel,
70    AdminCanceled,
71    AdminSettle,
72    AdminSettled,
73    AdminAddSolver,
74    AdminTakeDispute,
75    AdminTookDispute,
76    PaymentFailed,
77    InvoiceUpdated,
78    SendDm,
79    TradePubkey,
80    RestoreSession,
81}
82
83impl fmt::Display for Action {
84    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
85        write!(f, "{self:?}")
86    }
87}
88
89/// Use this Message to establish communication between users and Mostro
90#[derive(Debug, Clone, Deserialize, Serialize)]
91#[serde(rename_all = "kebab-case")]
92pub enum Message {
93    Order(MessageKind),
94    Dispute(MessageKind),
95    CantDo(MessageKind),
96    Rate(MessageKind),
97    Dm(MessageKind),
98    Restore(MessageKind),
99}
100
101impl Message {
102    /// New order message
103    pub fn new_order(
104        id: Option<Uuid>,
105        request_id: Option<u64>,
106        trade_index: Option<i64>,
107        action: Action,
108        payload: Option<Payload>,
109    ) -> Self {
110        let kind = MessageKind::new(id, request_id, trade_index, action, payload);
111        Self::Order(kind)
112    }
113
114    /// New dispute message
115    pub fn new_dispute(
116        id: Option<Uuid>,
117        request_id: Option<u64>,
118        trade_index: Option<i64>,
119        action: Action,
120        payload: Option<Payload>,
121    ) -> Self {
122        let kind = MessageKind::new(id, request_id, trade_index, action, payload);
123
124        Self::Dispute(kind)
125    }
126
127    pub fn new_restore(payload: Option<Payload>) -> Self {
128        let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
129        Self::Restore(kind)
130    }
131
132    /// New can't do template message message
133    pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
134        let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
135
136        Self::CantDo(kind)
137    }
138
139    /// New DM message
140    pub fn new_dm(
141        id: Option<Uuid>,
142        request_id: Option<u64>,
143        action: Action,
144        payload: Option<Payload>,
145    ) -> Self {
146        let kind = MessageKind::new(id, request_id, None, action, payload);
147
148        Self::Dm(kind)
149    }
150
151    /// Get message from json string
152    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
153        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
154    }
155
156    /// Get message as json string
157    pub fn as_json(&self) -> Result<String, ServiceError> {
158        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
159    }
160
161    // Get inner message kind
162    pub fn get_inner_message_kind(&self) -> &MessageKind {
163        match self {
164            Message::Dispute(k)
165            | Message::Order(k)
166            | Message::CantDo(k)
167            | Message::Rate(k)
168            | Message::Dm(k)
169            | Message::Restore(k) => k,
170        }
171    }
172
173    // Get action from the inner message
174    pub fn inner_action(&self) -> Option<Action> {
175        match self {
176            Message::Dispute(a)
177            | Message::Order(a)
178            | Message::CantDo(a)
179            | Message::Rate(a)
180            | Message::Dm(a)
181            | Message::Restore(a) => Some(a.get_action()),
182        }
183    }
184
185    /// Verify if is valid the inner message
186    pub fn verify(&self) -> bool {
187        match self {
188            Message::Order(m)
189            | Message::Dispute(m)
190            | Message::CantDo(m)
191            | Message::Rate(m)
192            | Message::Dm(m)
193            | Message::Restore(m) => m.verify(),
194        }
195    }
196
197    pub fn sign(message: String, keys: &Keys) -> Signature {
198        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
199        let hash = hash.to_byte_array();
200        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
201
202        keys.sign_schnorr(&message)
203    }
204
205    pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
206        // Create payload hash
207        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
208        let hash = hash.to_byte_array();
209        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
210
211        // Create a verification-only context for better performance
212        let secp = Secp256k1::verification_only();
213        // Verify signature
214        if let Ok(xonlykey) = pubkey.xonly() {
215            xonlykey.verify(&secp, &message, &sig).is_ok()
216        } else {
217            false
218        }
219    }
220}
221
222/// Use this Message to establish communication between users and Mostro
223#[derive(Debug, Clone, Deserialize, Serialize)]
224pub struct MessageKind {
225    /// Message version
226    pub version: u8,
227    /// Request_id for test on client
228    pub request_id: Option<u64>,
229    /// Trade key index
230    pub trade_index: Option<i64>,
231    /// Message id is not mandatory
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub id: Option<Uuid>,
234    /// Action to be taken
235    pub action: Action,
236    /// Payload of the Message
237    pub payload: Option<Payload>,
238}
239
240type Amount = i64;
241
242/// Payment failure retry information
243#[derive(Debug, Deserialize, Serialize, Clone)]
244pub struct PaymentFailedInfo {
245    /// Maximum number of payment attempts
246    pub payment_attempts: u32,
247    /// Retry interval in seconds between payment attempts
248    pub payment_retries_interval: u32,
249}
250
251/// Information about the order to be restored in the new client
252#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
253#[derive(Debug, Deserialize, Serialize, Clone)]
254pub struct RestoredOrdersInfo {
255    /// Id of the order
256    pub order_id: Uuid,
257    /// Trade index of the order
258    pub trade_index: i64,
259    /// Status of the order
260    pub status: String,
261}
262
263/// Information about the dispute to be restored in the new client
264#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
265#[derive(Debug, Deserialize, Serialize, Clone)]
266pub struct RestoredDisputesInfo {
267    /// Id of the dispute
268    pub dispute_id: Uuid,
269    /// Order id of the dispute
270    pub order_id: Uuid,
271    /// Trade index of the dispute
272    pub trade_index: i64,
273    /// Status of the dispute
274    pub status: String,
275}
276
277/// Restore session user info
278#[derive(Debug, Deserialize, Serialize, Clone, Default)]
279pub struct RestoreSessionInfo {
280    /// Vector of orders of the user requesting the restore of data
281    #[serde(rename = "orders")]
282    pub restore_orders: Vec<RestoredOrdersInfo>,
283    /// Vector of disputes of the user requesting the restore of data
284    #[serde(rename = "disputes")]
285    pub restore_disputes: Vec<RestoredDisputesInfo>,
286}
287
288/// Message payload
289#[derive(Debug, Deserialize, Serialize, Clone)]
290#[serde(rename_all = "snake_case")]
291pub enum Payload {
292    /// Order
293    Order(SmallOrder),
294    /// Payment request
295    PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
296    /// Use to send a message to another user
297    TextMessage(String),
298    /// Peer information
299    Peer(Peer),
300    /// Used to rate a user
301    RatingUser(u8),
302    /// In some cases we need to send an amount
303    Amount(Amount),
304    /// Dispute
305    Dispute(Uuid, Option<u16>, Option<SolverDisputeInfo>),
306    /// Here the reason why we can't do the action
307    CantDo(Option<CantDoReason>),
308    /// This is used by the maker of a range order only on
309    /// messages with action release and fiat-sent
310    /// to inform the next trade pubkey and trade index
311    NextTrade(String, u32),
312    /// Payment failure retry configuration information
313    PaymentFailed(PaymentFailedInfo),
314    /// Restore session data with orders and disputes
315    RestoreData(RestoreSessionInfo),
316}
317
318#[allow(dead_code)]
319impl MessageKind {
320    /// New message
321    pub fn new(
322        id: Option<Uuid>,
323        request_id: Option<u64>,
324        trade_index: Option<i64>,
325        action: Action,
326        payload: Option<Payload>,
327    ) -> Self {
328        Self {
329            version: PROTOCOL_VER,
330            request_id,
331            trade_index,
332            id,
333            action,
334            payload,
335        }
336    }
337    /// Get message from json string
338    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
339        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
340    }
341    /// Get message as json string
342    pub fn as_json(&self) -> Result<String, ServiceError> {
343        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
344    }
345
346    // Get action from the inner message
347    pub fn get_action(&self) -> Action {
348        self.action.clone()
349    }
350
351    /// Get the next trade keys when order is settled
352    pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
353        match &self.payload {
354            Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
355            None => Ok(None),
356            _ => Err(ServiceError::InvalidPayload),
357        }
358    }
359
360    pub fn get_rating(&self) -> Result<u8, ServiceError> {
361        if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
362            if !(MIN_RATING..=MAX_RATING).contains(&v) {
363                return Err(ServiceError::InvalidRatingValue);
364            }
365            Ok(v)
366        } else {
367            Err(ServiceError::InvalidRating)
368        }
369    }
370
371    /// Verify if is valid message
372    pub fn verify(&self) -> bool {
373        match &self.action {
374            Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
375            Action::PayInvoice | Action::AddInvoice => {
376                if self.id.is_none() {
377                    return false;
378                }
379                matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
380            }
381            Action::TakeSell
382            | Action::TakeBuy
383            | Action::FiatSent
384            | Action::FiatSentOk
385            | Action::Release
386            | Action::Released
387            | Action::Dispute
388            | Action::AdminCancel
389            | Action::AdminCanceled
390            | Action::AdminSettle
391            | Action::AdminSettled
392            | Action::Rate
393            | Action::RateReceived
394            | Action::AdminTakeDispute
395            | Action::AdminTookDispute
396            | Action::DisputeInitiatedByYou
397            | Action::DisputeInitiatedByPeer
398            | Action::WaitingBuyerInvoice
399            | Action::PurchaseCompleted
400            | Action::HoldInvoicePaymentAccepted
401            | Action::HoldInvoicePaymentSettled
402            | Action::HoldInvoicePaymentCanceled
403            | Action::WaitingSellerToPay
404            | Action::BuyerTookOrder
405            | Action::BuyerInvoiceAccepted
406            | Action::CooperativeCancelInitiatedByYou
407            | Action::CooperativeCancelInitiatedByPeer
408            | Action::CooperativeCancelAccepted
409            | Action::Cancel
410            | Action::InvoiceUpdated
411            | Action::AdminAddSolver
412            | Action::SendDm
413            | Action::TradePubkey
414            | Action::Canceled => {
415                if self.id.is_none() {
416                    return false;
417                }
418                true
419            }
420            Action::PaymentFailed => {
421                if self.id.is_none() {
422                    return false;
423                }
424                matches!(&self.payload, Some(Payload::PaymentFailed(_)))
425            }
426            Action::RateUser => {
427                matches!(&self.payload, Some(Payload::RatingUser(_)))
428            }
429            Action::CantDo => {
430                matches!(&self.payload, Some(Payload::CantDo(_)))
431            }
432            Action::RestoreSession => {
433                if self.id.is_some() || self.request_id.is_some() || self.trade_index.is_some() {
434                    return false;
435                }
436                matches!(&self.payload, None | Some(Payload::RestoreData(_)))
437            }
438        }
439    }
440
441    pub fn get_order(&self) -> Option<&SmallOrder> {
442        if self.action != Action::NewOrder {
443            return None;
444        }
445        match &self.payload {
446            Some(Payload::Order(o)) => Some(o),
447            _ => None,
448        }
449    }
450
451    pub fn get_payment_request(&self) -> Option<String> {
452        if self.action != Action::TakeSell
453            && self.action != Action::AddInvoice
454            && self.action != Action::NewOrder
455        {
456            return None;
457        }
458        match &self.payload {
459            Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
460            Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
461            _ => None,
462        }
463    }
464
465    pub fn get_amount(&self) -> Option<Amount> {
466        if self.action != Action::TakeSell && self.action != Action::TakeBuy {
467            return None;
468        }
469        match &self.payload {
470            Some(Payload::PaymentRequest(_, _, amount)) => *amount,
471            Some(Payload::Amount(amount)) => Some(*amount),
472            _ => None,
473        }
474    }
475
476    pub fn get_payload(&self) -> Option<&Payload> {
477        self.payload.as_ref()
478    }
479
480    pub fn has_trade_index(&self) -> (bool, i64) {
481        if let Some(index) = self.trade_index {
482            return (true, index);
483        }
484        (false, 0)
485    }
486
487    pub fn trade_index(&self) -> i64 {
488        if let Some(index) = self.trade_index {
489            return index;
490        }
491        0
492    }
493}
494
495#[cfg(test)]
496mod test {
497    use crate::message::{Action, Message, MessageKind, Payload, Peer};
498    use crate::user::UserInfo;
499    use nostr_sdk::Keys;
500    use uuid::uuid;
501
502    #[test]
503    fn test_peer_with_reputation() {
504        // Test creating a Peer with reputation information
505        let reputation = UserInfo {
506            rating: 4.5,
507            reviews: 10,
508            operating_days: 30,
509        };
510        let peer = Peer::new(
511            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
512            Some(reputation.clone()),
513        );
514
515        // Assert the fields are set correctly
516        assert_eq!(
517            peer.pubkey,
518            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
519        );
520        assert!(peer.reputation.is_some());
521        let peer_reputation = peer.reputation.clone().unwrap();
522        assert_eq!(peer_reputation.rating, 4.5);
523        assert_eq!(peer_reputation.reviews, 10);
524        assert_eq!(peer_reputation.operating_days, 30);
525
526        // Test JSON serialization and deserialization
527        let json = peer.as_json().unwrap();
528        let deserialized_peer = Peer::from_json(&json).unwrap();
529        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
530        assert!(deserialized_peer.reputation.is_some());
531        let deserialized_reputation = deserialized_peer.reputation.unwrap();
532        assert_eq!(deserialized_reputation.rating, 4.5);
533        assert_eq!(deserialized_reputation.reviews, 10);
534        assert_eq!(deserialized_reputation.operating_days, 30);
535    }
536
537    #[test]
538    fn test_peer_without_reputation() {
539        // Test creating a Peer without reputation information
540        let peer = Peer::new(
541            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
542            None,
543        );
544
545        // Assert the reputation field is None
546        assert_eq!(
547            peer.pubkey,
548            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
549        );
550        assert!(peer.reputation.is_none());
551
552        // Test JSON serialization and deserialization
553        let json = peer.as_json().unwrap();
554        let deserialized_peer = Peer::from_json(&json).unwrap();
555        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
556        assert!(deserialized_peer.reputation.is_none());
557    }
558
559    #[test]
560    fn test_peer_in_message() {
561        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
562
563        // Test with reputation
564        let reputation = UserInfo {
565            rating: 4.5,
566            reviews: 10,
567            operating_days: 30,
568        };
569        let peer_with_reputation = Peer::new(
570            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
571            Some(reputation),
572        );
573        let payload_with_reputation = Payload::Peer(peer_with_reputation);
574        let message_with_reputation = Message::Order(MessageKind::new(
575            Some(uuid),
576            Some(1),
577            Some(2),
578            Action::FiatSentOk,
579            Some(payload_with_reputation),
580        ));
581
582        // Verify message with reputation
583        assert!(message_with_reputation.verify());
584        let message_json = message_with_reputation.as_json().unwrap();
585        let deserialized_message = Message::from_json(&message_json).unwrap();
586        assert!(deserialized_message.verify());
587
588        // Test without reputation
589        let peer_without_reputation = Peer::new(
590            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
591            None,
592        );
593        let payload_without_reputation = Payload::Peer(peer_without_reputation);
594        let message_without_reputation = Message::Order(MessageKind::new(
595            Some(uuid),
596            Some(1),
597            Some(2),
598            Action::FiatSentOk,
599            Some(payload_without_reputation),
600        ));
601
602        // Verify message without reputation
603        assert!(message_without_reputation.verify());
604        let message_json = message_without_reputation.as_json().unwrap();
605        let deserialized_message = Message::from_json(&message_json).unwrap();
606        assert!(deserialized_message.verify());
607    }
608
609    #[test]
610    fn test_payment_failed_payload() {
611        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
612
613        // Test PaymentFailedInfo serialization and deserialization
614        let payment_failed_info = crate::message::PaymentFailedInfo {
615            payment_attempts: 3,
616            payment_retries_interval: 60,
617        };
618
619        let payload = Payload::PaymentFailed(payment_failed_info);
620        let message = Message::Order(MessageKind::new(
621            Some(uuid),
622            Some(1),
623            Some(2),
624            Action::PaymentFailed,
625            Some(payload),
626        ));
627
628        // Verify message validation
629        assert!(message.verify());
630
631        // Test JSON serialization
632        let message_json = message.as_json().unwrap();
633
634        // Test deserialization
635        let deserialized_message = Message::from_json(&message_json).unwrap();
636        assert!(deserialized_message.verify());
637
638        // Verify the payload contains correct values
639        if let Message::Order(kind) = deserialized_message {
640            if let Some(Payload::PaymentFailed(info)) = kind.payload {
641                assert_eq!(info.payment_attempts, 3);
642                assert_eq!(info.payment_retries_interval, 60);
643            } else {
644                panic!("Expected PaymentFailed payload");
645            }
646        } else {
647            panic!("Expected Order message");
648        }
649    }
650
651    #[test]
652    fn test_message_payload_signature() {
653        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
654        let peer = Peer::new(
655            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
656            None, // Add None for the reputation parameter
657        );
658        let payload = Payload::Peer(peer);
659        let test_message = Message::Order(MessageKind::new(
660            Some(uuid),
661            Some(1),
662            Some(2),
663            Action::FiatSentOk,
664            Some(payload),
665        ));
666        assert!(test_message.verify());
667        let test_message_json = test_message.as_json().unwrap();
668        // Message should be signed with the trade keys
669        let trade_keys =
670            Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
671                .unwrap();
672        let sig = Message::sign(test_message_json.clone(), &trade_keys);
673
674        assert!(Message::verify_signature(
675            test_message_json,
676            trade_keys.public_key(),
677            sig
678        ));
679    }
680
681    #[test]
682    fn test_restore_session_message() {
683        // Test RestoreSession request (payload = None)
684        let restore_request_message = Message::Restore(MessageKind::new(
685            None,
686            None,
687            None,
688            Action::RestoreSession,
689            None,
690        ));
691
692        // Verify message validation
693        assert!(restore_request_message.verify());
694        assert_eq!(
695            restore_request_message.inner_action(),
696            Some(Action::RestoreSession)
697        );
698
699        // Test JSON serialization and deserialization for RestoreRequest
700        let message_json = restore_request_message.as_json().unwrap();
701        let deserialized_message = Message::from_json(&message_json).unwrap();
702        assert!(deserialized_message.verify());
703        assert_eq!(
704            deserialized_message.inner_action(),
705            Some(Action::RestoreSession)
706        );
707
708        // Test RestoreSession with RestoreData payload
709        let restored_orders = vec![
710            crate::message::RestoredOrdersInfo {
711                order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
712                trade_index: 1,
713                status: "active".to_string(),
714            },
715            crate::message::RestoredOrdersInfo {
716                order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
717                trade_index: 2,
718                status: "success".to_string(),
719            },
720        ];
721
722        let restored_disputes = vec![crate::message::RestoredDisputesInfo {
723            dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
724            order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
725            trade_index: 1,
726            status: "initiated".to_string(),
727        }];
728
729        let restore_session_info = crate::message::RestoreSessionInfo {
730            restore_orders: restored_orders.clone(),
731            restore_disputes: restored_disputes.clone(),
732        };
733
734        let restore_data_payload = Payload::RestoreData(restore_session_info);
735        let restore_data_message = Message::Restore(MessageKind::new(
736            None,
737            None,
738            None,
739            Action::RestoreSession,
740            Some(restore_data_payload),
741        ));
742
743        // Verify message validation
744        assert!(restore_data_message.verify());
745        assert_eq!(
746            restore_data_message.inner_action(),
747            Some(Action::RestoreSession)
748        );
749
750        // Test JSON serialization and deserialization for RestoreData
751        let message_json = restore_data_message.as_json().unwrap();
752        let deserialized_message = Message::from_json(&message_json).unwrap();
753        assert!(deserialized_message.verify());
754        assert_eq!(
755            deserialized_message.inner_action(),
756            Some(Action::RestoreSession)
757        );
758
759        // Verify the payload contains correct data
760        if let Message::Restore(kind) = deserialized_message {
761            if let Some(Payload::RestoreData(info)) = kind.payload {
762                assert_eq!(info.restore_orders.len(), 2);
763                assert_eq!(info.restore_disputes.len(), 1);
764
765                // Check first order
766                assert_eq!(
767                    info.restore_orders[0].order_id,
768                    uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")
769                );
770                assert_eq!(info.restore_orders[0].trade_index, 1);
771                assert_eq!(info.restore_orders[0].status, "active");
772
773                // Check second order
774                assert_eq!(
775                    info.restore_orders[1].order_id,
776                    uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24")
777                );
778                assert_eq!(info.restore_orders[1].trade_index, 2);
779                assert_eq!(info.restore_orders[1].status, "success");
780
781                // Check dispute
782                assert_eq!(
783                    info.restore_disputes[0].dispute_id,
784                    uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25")
785                );
786                assert_eq!(
787                    info.restore_disputes[0].order_id,
788                    uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")
789                );
790                assert_eq!(info.restore_disputes[0].trade_index, 1);
791                assert_eq!(info.restore_disputes[0].status, "initiated");
792            } else {
793                panic!("Expected RestoreData payload");
794            }
795        } else {
796            panic!("Expected Restore message");
797        }
798    }
799
800    #[test]
801    fn test_restore_session_message_validation() {
802        // Test that RestoreSession action accepts only payload=None or RestoreData
803        let restore_request_message = Message::Restore(MessageKind::new(
804            None,
805            None,
806            None,
807            Action::RestoreSession,
808            None, // Missing payload
809        ));
810
811        // Verify restore request message
812        assert!(restore_request_message.verify());
813
814        // Test with wrong payload type
815        let wrong_payload = Payload::TextMessage("wrong payload".to_string());
816        let wrong_message = Message::Restore(MessageKind::new(
817            None,
818            None,
819            None,
820            Action::RestoreSession,
821            Some(wrong_payload),
822        ));
823
824        // Should fail validation because RestoreSession only accepts None or RestoreData
825        assert!(!wrong_message.verify());
826
827        // Id presence should make it invalid
828        let with_id = Message::Restore(MessageKind::new(
829            Some(uuid!("00000000-0000-0000-0000-000000000001")),
830            None,
831            None,
832            Action::RestoreSession,
833            None,
834        ));
835        assert!(!with_id.verify());
836
837        // request_id presence should make it invalid
838        let with_request_id = Message::Restore(MessageKind::new(
839            None,
840            Some(42),
841            None,
842            Action::RestoreSession,
843            None,
844        ));
845        assert!(!with_request_id.verify());
846
847        // trade_index presence should make it invalid
848        let with_trade_index = Message::Restore(MessageKind::new(
849            None,
850            None,
851            Some(7),
852            Action::RestoreSession,
853            None,
854        ));
855        assert!(!with_trade_index.verify());
856    }
857
858    #[test]
859    fn test_restore_session_message_constructor() {
860        // Test the new_restore constructor
861        let restore_request_message = Message::new_restore(None);
862
863        assert!(matches!(restore_request_message, Message::Restore(_)));
864        assert!(restore_request_message.verify());
865        assert_eq!(
866            restore_request_message.inner_action(),
867            Some(Action::RestoreSession)
868        );
869
870        // Test with RestoreData payload
871        let restore_session_info = crate::message::RestoreSessionInfo {
872            restore_orders: vec![],
873            restore_disputes: vec![],
874        };
875        let restore_data_message =
876            Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
877
878        assert!(matches!(restore_data_message, Message::Restore(_)));
879        assert!(restore_data_message.verify());
880        assert_eq!(
881            restore_data_message.inner_action(),
882            Some(Action::RestoreSession)
883        );
884    }
885}