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