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