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