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