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