Skip to main content

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/// 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    /// Indicates whether the buyer has initiated a dispute for this order.
279    /// Used in conjunction with `seller_dispute` to derive the `initiator` field in `RestoredDisputesInfo`.
280    pub buyer_dispute: bool,
281    /// Indicates whether the seller has initiated a dispute for this order.
282    /// Used in conjunction with `buyer_dispute` to derive the `initiator` field in `RestoredDisputesInfo`.
283    pub seller_dispute: bool,
284}
285
286/// Information about the order to be restored in the new client
287#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
288#[derive(Debug, Deserialize, Serialize, Clone)]
289pub struct RestoredOrdersInfo {
290    /// Id of the order
291    pub order_id: Uuid,
292    /// Trade index of the order
293    pub trade_index: i64,
294    /// Status of the order
295    pub status: String,
296}
297
298/// Enum representing who initiated a dispute
299#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
300#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
301#[serde(rename_all = "lowercase")]
302#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
303pub enum DisputeInitiator {
304    Buyer,
305    Seller,
306}
307
308/// Information about the dispute to be restored in the new client
309#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
310#[derive(Debug, Deserialize, Serialize, Clone)]
311pub struct RestoredDisputesInfo {
312    /// Id of the dispute
313    pub dispute_id: Uuid,
314    /// Order id of the dispute
315    pub order_id: Uuid,
316    /// Trade index of the dispute
317    pub trade_index: i64,
318    /// Status of the dispute
319    pub status: String,
320    /// Who initiated the dispute: Buyer, Seller, or null if unknown
321    pub initiator: Option<DisputeInitiator>,
322}
323
324/// Restore session user info
325#[derive(Debug, Deserialize, Serialize, Clone, Default)]
326pub struct RestoreSessionInfo {
327    /// Vector of orders of the user requesting the restore of data
328    #[serde(rename = "orders")]
329    pub restore_orders: Vec<RestoredOrdersInfo>,
330    /// Vector of disputes of the user requesting the restore of data
331    #[serde(rename = "disputes")]
332    pub restore_disputes: Vec<RestoredDisputesInfo>,
333}
334
335/// Message payload
336#[derive(Debug, Deserialize, Serialize, Clone)]
337#[serde(rename_all = "snake_case")]
338pub enum Payload {
339    /// Order
340    Order(SmallOrder),
341    /// Payment request
342    PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
343    /// Use to send a message to another user
344    TextMessage(String),
345    /// Peer information
346    Peer(Peer),
347    /// Used to rate a user
348    RatingUser(u8),
349    /// In some cases we need to send an amount
350    Amount(Amount),
351    /// Dispute
352    Dispute(Uuid, Option<SolverDisputeInfo>),
353    /// Here the reason why we can't do the action
354    CantDo(Option<CantDoReason>),
355    /// This is used by the maker of a range order only on
356    /// messages with action release and fiat-sent
357    /// to inform the next trade pubkey and trade index
358    NextTrade(String, u32),
359    /// Payment failure retry configuration information
360    PaymentFailed(PaymentFailedInfo),
361    /// Restore session data with orders and disputes
362    RestoreData(RestoreSessionInfo),
363    /// IDs array
364    Ids(Vec<Uuid>),
365    /// Orders array
366    Orders(Vec<SmallOrder>),
367}
368
369#[allow(dead_code)]
370impl MessageKind {
371    /// New message
372    pub fn new(
373        id: Option<Uuid>,
374        request_id: Option<u64>,
375        trade_index: Option<i64>,
376        action: Action,
377        payload: Option<Payload>,
378    ) -> Self {
379        Self {
380            version: PROTOCOL_VER,
381            request_id,
382            trade_index,
383            id,
384            action,
385            payload,
386        }
387    }
388    /// Get message from json string
389    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
390        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
391    }
392    /// Get message as json string
393    pub fn as_json(&self) -> Result<String, ServiceError> {
394        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
395    }
396
397    // Get action from the inner message
398    pub fn get_action(&self) -> Action {
399        self.action.clone()
400    }
401
402    /// Get the next trade keys when order is settled
403    pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
404        match &self.payload {
405            Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
406            None => Ok(None),
407            _ => Err(ServiceError::InvalidPayload),
408        }
409    }
410
411    pub fn get_rating(&self) -> Result<u8, ServiceError> {
412        if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
413            if !(MIN_RATING..=MAX_RATING).contains(&v) {
414                return Err(ServiceError::InvalidRatingValue);
415            }
416            Ok(v)
417        } else {
418            Err(ServiceError::InvalidRating)
419        }
420    }
421
422    /// Verify if is valid message
423    pub fn verify(&self) -> bool {
424        match &self.action {
425            Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
426            Action::PayInvoice | Action::AddInvoice => {
427                if self.id.is_none() {
428                    return false;
429                }
430                matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
431            }
432            Action::TakeSell
433            | Action::TakeBuy
434            | Action::FiatSent
435            | Action::FiatSentOk
436            | Action::Release
437            | Action::Released
438            | Action::Dispute
439            | Action::AdminCancel
440            | Action::AdminCanceled
441            | Action::AdminSettle
442            | Action::AdminSettled
443            | Action::Rate
444            | Action::RateReceived
445            | Action::AdminTakeDispute
446            | Action::AdminTookDispute
447            | Action::DisputeInitiatedByYou
448            | Action::DisputeInitiatedByPeer
449            | Action::WaitingBuyerInvoice
450            | Action::PurchaseCompleted
451            | Action::HoldInvoicePaymentAccepted
452            | Action::HoldInvoicePaymentSettled
453            | Action::HoldInvoicePaymentCanceled
454            | Action::WaitingSellerToPay
455            | Action::BuyerTookOrder
456            | Action::BuyerInvoiceAccepted
457            | Action::CooperativeCancelInitiatedByYou
458            | Action::CooperativeCancelInitiatedByPeer
459            | Action::CooperativeCancelAccepted
460            | Action::Cancel
461            | Action::InvoiceUpdated
462            | Action::AdminAddSolver
463            | Action::SendDm
464            | Action::TradePubkey
465            | Action::Canceled => {
466                if self.id.is_none() {
467                    return false;
468                }
469                true
470            }
471            Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
472            Action::PaymentFailed => {
473                if self.id.is_none() {
474                    return false;
475                }
476                matches!(&self.payload, Some(Payload::PaymentFailed(_)))
477            }
478            Action::RateUser => {
479                matches!(&self.payload, Some(Payload::RatingUser(_)))
480            }
481            Action::CantDo => {
482                matches!(&self.payload, Some(Payload::CantDo(_)))
483            }
484            Action::Orders => {
485                matches!(
486                    &self.payload,
487                    Some(Payload::Ids(_)) | Some(Payload::Orders(_))
488                )
489            }
490        }
491    }
492
493    pub fn get_order(&self) -> Option<&SmallOrder> {
494        if self.action != Action::NewOrder {
495            return None;
496        }
497        match &self.payload {
498            Some(Payload::Order(o)) => Some(o),
499            _ => None,
500        }
501    }
502
503    pub fn get_payment_request(&self) -> Option<String> {
504        if self.action != Action::TakeSell
505            && self.action != Action::AddInvoice
506            && self.action != Action::NewOrder
507        {
508            return None;
509        }
510        match &self.payload {
511            Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
512            Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
513            _ => None,
514        }
515    }
516
517    pub fn get_amount(&self) -> Option<Amount> {
518        if self.action != Action::TakeSell && self.action != Action::TakeBuy {
519            return None;
520        }
521        match &self.payload {
522            Some(Payload::PaymentRequest(_, _, amount)) => *amount,
523            Some(Payload::Amount(amount)) => Some(*amount),
524            _ => None,
525        }
526    }
527
528    pub fn get_payload(&self) -> Option<&Payload> {
529        self.payload.as_ref()
530    }
531
532    pub fn has_trade_index(&self) -> (bool, i64) {
533        if let Some(index) = self.trade_index {
534            return (true, index);
535        }
536        (false, 0)
537    }
538
539    pub fn trade_index(&self) -> i64 {
540        if let Some(index) = self.trade_index {
541            return index;
542        }
543        0
544    }
545}
546
547#[cfg(test)]
548mod test {
549    use crate::message::{Action, Message, MessageKind, Payload, Peer};
550    use crate::user::UserInfo;
551    use nostr_sdk::Keys;
552    use uuid::uuid;
553
554    #[test]
555    fn test_peer_with_reputation() {
556        // Test creating a Peer with reputation information
557        let reputation = UserInfo {
558            rating: 4.5,
559            reviews: 10,
560            operating_days: 30,
561        };
562        let peer = Peer::new(
563            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
564            Some(reputation.clone()),
565        );
566
567        // Assert the fields are set correctly
568        assert_eq!(
569            peer.pubkey,
570            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
571        );
572        assert!(peer.reputation.is_some());
573        let peer_reputation = peer.reputation.clone().unwrap();
574        assert_eq!(peer_reputation.rating, 4.5);
575        assert_eq!(peer_reputation.reviews, 10);
576        assert_eq!(peer_reputation.operating_days, 30);
577
578        // Test JSON serialization and deserialization
579        let json = peer.as_json().unwrap();
580        let deserialized_peer = Peer::from_json(&json).unwrap();
581        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
582        assert!(deserialized_peer.reputation.is_some());
583        let deserialized_reputation = deserialized_peer.reputation.unwrap();
584        assert_eq!(deserialized_reputation.rating, 4.5);
585        assert_eq!(deserialized_reputation.reviews, 10);
586        assert_eq!(deserialized_reputation.operating_days, 30);
587    }
588
589    #[test]
590    fn test_peer_without_reputation() {
591        // Test creating a Peer without reputation information
592        let peer = Peer::new(
593            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
594            None,
595        );
596
597        // Assert the reputation field is None
598        assert_eq!(
599            peer.pubkey,
600            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
601        );
602        assert!(peer.reputation.is_none());
603
604        // Test JSON serialization and deserialization
605        let json = peer.as_json().unwrap();
606        let deserialized_peer = Peer::from_json(&json).unwrap();
607        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
608        assert!(deserialized_peer.reputation.is_none());
609    }
610
611    #[test]
612    fn test_peer_in_message() {
613        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
614
615        // Test with reputation
616        let reputation = UserInfo {
617            rating: 4.5,
618            reviews: 10,
619            operating_days: 30,
620        };
621        let peer_with_reputation = Peer::new(
622            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
623            Some(reputation),
624        );
625        let payload_with_reputation = Payload::Peer(peer_with_reputation);
626        let message_with_reputation = Message::Order(MessageKind::new(
627            Some(uuid),
628            Some(1),
629            Some(2),
630            Action::FiatSentOk,
631            Some(payload_with_reputation),
632        ));
633
634        // Verify message with reputation
635        assert!(message_with_reputation.verify());
636        let message_json = message_with_reputation.as_json().unwrap();
637        let deserialized_message = Message::from_json(&message_json).unwrap();
638        assert!(deserialized_message.verify());
639
640        // Test without reputation
641        let peer_without_reputation = Peer::new(
642            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
643            None,
644        );
645        let payload_without_reputation = Payload::Peer(peer_without_reputation);
646        let message_without_reputation = Message::Order(MessageKind::new(
647            Some(uuid),
648            Some(1),
649            Some(2),
650            Action::FiatSentOk,
651            Some(payload_without_reputation),
652        ));
653
654        // Verify message without reputation
655        assert!(message_without_reputation.verify());
656        let message_json = message_without_reputation.as_json().unwrap();
657        let deserialized_message = Message::from_json(&message_json).unwrap();
658        assert!(deserialized_message.verify());
659    }
660
661    #[test]
662    fn test_payment_failed_payload() {
663        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
664
665        // Test PaymentFailedInfo serialization and deserialization
666        let payment_failed_info = crate::message::PaymentFailedInfo {
667            payment_attempts: 3,
668            payment_retries_interval: 60,
669        };
670
671        let payload = Payload::PaymentFailed(payment_failed_info);
672        let message = Message::Order(MessageKind::new(
673            Some(uuid),
674            Some(1),
675            Some(2),
676            Action::PaymentFailed,
677            Some(payload),
678        ));
679
680        // Verify message validation
681        assert!(message.verify());
682
683        // Test JSON serialization
684        let message_json = message.as_json().unwrap();
685
686        // Test deserialization
687        let deserialized_message = Message::from_json(&message_json).unwrap();
688        assert!(deserialized_message.verify());
689
690        // Verify the payload contains correct values
691        if let Message::Order(kind) = deserialized_message {
692            if let Some(Payload::PaymentFailed(info)) = kind.payload {
693                assert_eq!(info.payment_attempts, 3);
694                assert_eq!(info.payment_retries_interval, 60);
695            } else {
696                panic!("Expected PaymentFailed payload");
697            }
698        } else {
699            panic!("Expected Order message");
700        }
701    }
702
703    #[test]
704    fn test_message_payload_signature() {
705        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
706        let peer = Peer::new(
707            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
708            None, // Add None for the reputation parameter
709        );
710        let payload = Payload::Peer(peer);
711        let test_message = Message::Order(MessageKind::new(
712            Some(uuid),
713            Some(1),
714            Some(2),
715            Action::FiatSentOk,
716            Some(payload),
717        ));
718        assert!(test_message.verify());
719        let test_message_json = test_message.as_json().unwrap();
720        // Message should be signed with the trade keys
721        let trade_keys =
722            Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
723                .unwrap();
724        let sig = Message::sign(test_message_json.clone(), &trade_keys);
725
726        assert!(Message::verify_signature(
727            test_message_json,
728            trade_keys.public_key(),
729            sig
730        ));
731    }
732
733    #[test]
734    fn test_restore_session_message() {
735        // Test RestoreSession request (payload = None)
736        let restore_request_message = Message::Restore(MessageKind::new(
737            None,
738            None,
739            None,
740            Action::RestoreSession,
741            None,
742        ));
743
744        // Verify message validation
745        assert!(restore_request_message.verify());
746        assert_eq!(
747            restore_request_message.inner_action(),
748            Some(Action::RestoreSession)
749        );
750
751        // Test JSON serialization and deserialization for RestoreRequest
752        let message_json = restore_request_message.as_json().unwrap();
753        let deserialized_message = Message::from_json(&message_json).unwrap();
754        assert!(deserialized_message.verify());
755        assert_eq!(
756            deserialized_message.inner_action(),
757            Some(Action::RestoreSession)
758        );
759
760        // Test RestoreSession with RestoreData payload
761        let restored_orders = vec![
762            crate::message::RestoredOrdersInfo {
763                order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
764                trade_index: 1,
765                status: "active".to_string(),
766            },
767            crate::message::RestoredOrdersInfo {
768                order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
769                trade_index: 2,
770                status: "success".to_string(),
771            },
772        ];
773
774        let restored_disputes = vec![
775            crate::message::RestoredDisputesInfo {
776                dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
777                order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
778                trade_index: 1,
779                status: "initiated".to_string(),
780                initiator: Some(crate::message::DisputeInitiator::Buyer),
781            },
782            crate::message::RestoredDisputesInfo {
783                dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
784                order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
785                trade_index: 2,
786                status: "in-progress".to_string(),
787                initiator: None,
788            },
789            crate::message::RestoredDisputesInfo {
790                dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
791                order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
792                trade_index: 3,
793                status: "initiated".to_string(),
794                initiator: Some(crate::message::DisputeInitiator::Seller),
795            },
796        ];
797
798        let restore_session_info = crate::message::RestoreSessionInfo {
799            restore_orders: restored_orders.clone(),
800            restore_disputes: restored_disputes.clone(),
801        };
802
803        let restore_data_payload = Payload::RestoreData(restore_session_info);
804        let restore_data_message = Message::Restore(MessageKind::new(
805            None,
806            None,
807            None,
808            Action::RestoreSession,
809            Some(restore_data_payload),
810        ));
811
812        // With new logic, any payload for RestoreSession is invalid (must be None)
813        assert!(!restore_data_message.verify());
814
815        // Verify serialization/deserialization of RestoreData payload with all initiator cases
816        let message_json = restore_data_message.as_json().unwrap();
817        let deserialized_restore_message = Message::from_json(&message_json).unwrap();
818
819        if let Message::Restore(kind) = deserialized_restore_message {
820            if let Some(Payload::RestoreData(session_info)) = kind.payload {
821                assert_eq!(session_info.restore_disputes.len(), 3);
822                assert_eq!(
823                    session_info.restore_disputes[0].initiator,
824                    Some(crate::message::DisputeInitiator::Buyer)
825                );
826                assert_eq!(session_info.restore_disputes[1].initiator, None);
827                assert_eq!(
828                    session_info.restore_disputes[2].initiator,
829                    Some(crate::message::DisputeInitiator::Seller)
830                );
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
864        assert!(!wrong_message.verify());
865
866        // With new logic, presence of id/request_id/trade_index is allowed
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        let with_request_id = Message::Restore(MessageKind::new(
877            None,
878            Some(42),
879            None,
880            Action::RestoreSession,
881            None,
882        ));
883        assert!(with_request_id.verify());
884
885        let with_trade_index = Message::Restore(MessageKind::new(
886            None,
887            None,
888            Some(7),
889            Action::RestoreSession,
890            None,
891        ));
892        assert!(with_trade_index.verify());
893    }
894
895    #[test]
896    fn test_restore_session_message_constructor() {
897        // Test the new_restore constructor
898        let restore_request_message = Message::new_restore(None);
899
900        assert!(matches!(restore_request_message, Message::Restore(_)));
901        assert!(restore_request_message.verify());
902        assert_eq!(
903            restore_request_message.inner_action(),
904            Some(Action::RestoreSession)
905        );
906
907        // Test with RestoreData payload should be invalid now
908        let restore_session_info = crate::message::RestoreSessionInfo {
909            restore_orders: vec![],
910            restore_disputes: vec![],
911        };
912        let restore_data_message =
913            Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
914
915        assert!(matches!(restore_data_message, Message::Restore(_)));
916        assert!(!restore_data_message.verify());
917    }
918
919    #[test]
920    fn test_last_trade_index_valid_message() {
921        let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
922        let msg = Message::Restore(kind);
923
924        assert!(msg.verify());
925
926        // roundtrip
927        let json = msg.as_json().unwrap();
928        let decoded = Message::from_json(&json).unwrap();
929        assert!(decoded.verify());
930
931        // ensure the trade index is propagated
932        let inner = decoded.get_inner_message_kind();
933        assert_eq!(inner.trade_index(), 7);
934        assert_eq!(inner.has_trade_index(), (true, 7));
935    }
936
937    #[test]
938    fn test_last_trade_index_without_id_is_valid() {
939        // With new logic, id is not required; only payload must be None
940        let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
941        let msg = Message::Restore(kind);
942        assert!(msg.verify());
943    }
944
945    #[test]
946    fn test_last_trade_index_with_payload_fails_validation() {
947        // LastTradeIndex does not accept payload
948        let kind = MessageKind::new(
949            None,
950            None,
951            Some(3),
952            Action::LastTradeIndex,
953            Some(Payload::TextMessage("ignored".to_string())),
954        );
955        let msg = Message::Restore(kind);
956        assert!(!msg.verify());
957    }
958
959    #[test]
960    fn test_restored_dispute_helper_serialization_roundtrip() {
961        use crate::message::RestoredDisputeHelper;
962
963        let helper = RestoredDisputeHelper {
964            dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
965            order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
966            dispute_status: "initiated".to_string(),
967            master_buyer_pubkey: Some("npub1buyerkey".to_string()),
968            master_seller_pubkey: Some("npub1sellerkey".to_string()),
969            trade_index_buyer: Some(1),
970            trade_index_seller: Some(2),
971            buyer_dispute: true,
972            seller_dispute: false,
973        };
974
975        let json = serde_json::to_string(&helper).unwrap();
976        let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
977
978        assert_eq!(deserialized.dispute_id, helper.dispute_id);
979        assert_eq!(deserialized.order_id, helper.order_id);
980        assert_eq!(deserialized.dispute_status, helper.dispute_status);
981        assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
982        assert_eq!(
983            deserialized.master_seller_pubkey,
984            helper.master_seller_pubkey
985        );
986        assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
987        assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
988        assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
989        assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
990
991        let helper_seller_dispute = RestoredDisputeHelper {
992            dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
993            order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
994            dispute_status: "in-progress".to_string(),
995            master_buyer_pubkey: None,
996            master_seller_pubkey: None,
997            trade_index_buyer: None,
998            trade_index_seller: None,
999            buyer_dispute: false,
1000            seller_dispute: true,
1001        };
1002
1003        let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
1004        let deserialized_seller: RestoredDisputeHelper =
1005            serde_json::from_str(&json_seller).unwrap();
1006
1007        assert_eq!(
1008            deserialized_seller.dispute_id,
1009            helper_seller_dispute.dispute_id
1010        );
1011        assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
1012        assert_eq!(
1013            deserialized_seller.dispute_status,
1014            helper_seller_dispute.dispute_status
1015        );
1016        assert_eq!(deserialized_seller.master_buyer_pubkey, None);
1017        assert_eq!(deserialized_seller.master_seller_pubkey, None);
1018        assert_eq!(deserialized_seller.trade_index_buyer, None);
1019        assert_eq!(deserialized_seller.trade_index_seller, None);
1020        assert!(!deserialized_seller.buyer_dispute);
1021        assert!(deserialized_seller.seller_dispute);
1022    }
1023}