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
8use std::fmt;
9use uuid::Uuid;
10
11/// One party of the trade
12#[derive(Debug, Deserialize, Serialize, Clone)]
13pub struct Peer {
14    pub pubkey: String,
15    pub reputation: Option<UserInfo>,
16}
17
18impl Peer {
19    pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
20        Self { pubkey, reputation }
21    }
22
23    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
24        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
25    }
26
27    pub fn as_json(&self) -> Result<String, ServiceError> {
28        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
29    }
30}
31
32/// Action is used to identify each message between Mostro and users
33#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
34#[serde(rename_all = "kebab-case")]
35pub enum Action {
36    NewOrder,
37    TakeSell,
38    TakeBuy,
39    PayInvoice,
40    FiatSent,
41    FiatSentOk,
42    Release,
43    Released,
44    Cancel,
45    Canceled,
46    CooperativeCancelInitiatedByYou,
47    CooperativeCancelInitiatedByPeer,
48    DisputeInitiatedByYou,
49    DisputeInitiatedByPeer,
50    CooperativeCancelAccepted,
51    BuyerInvoiceAccepted,
52    PurchaseCompleted,
53    HoldInvoicePaymentAccepted,
54    HoldInvoicePaymentSettled,
55    HoldInvoicePaymentCanceled,
56    WaitingSellerToPay,
57    WaitingBuyerInvoice,
58    AddInvoice,
59    BuyerTookOrder,
60    Rate,
61    RateUser,
62    RateReceived,
63    CantDo,
64    Dispute,
65    AdminCancel,
66    AdminCanceled,
67    AdminSettle,
68    AdminSettled,
69    AdminAddSolver,
70    AdminTakeDispute,
71    AdminTookDispute,
72    PaymentFailed,
73    InvoiceUpdated,
74    SendDm,
75    TradePubkey,
76}
77
78impl fmt::Display for Action {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        write!(f, "{self:?}")
81    }
82}
83
84/// Use this Message to establish communication between users and Mostro
85#[derive(Debug, Clone, Deserialize, Serialize)]
86#[serde(rename_all = "kebab-case")]
87pub enum Message {
88    Order(MessageKind),
89    Dispute(MessageKind),
90    CantDo(MessageKind),
91    Rate(MessageKind),
92    Dm(MessageKind),
93}
94
95impl Message {
96    /// New order message
97    pub fn new_order(
98        id: Option<Uuid>,
99        request_id: Option<u64>,
100        trade_index: Option<i64>,
101        action: Action,
102        payload: Option<Payload>,
103    ) -> Self {
104        let kind = MessageKind::new(id, request_id, trade_index, action, payload);
105        Self::Order(kind)
106    }
107
108    /// New dispute message
109    pub fn new_dispute(
110        id: Option<Uuid>,
111        request_id: Option<u64>,
112        trade_index: Option<i64>,
113        action: Action,
114        payload: Option<Payload>,
115    ) -> Self {
116        let kind = MessageKind::new(id, request_id, trade_index, action, payload);
117
118        Self::Dispute(kind)
119    }
120
121    /// New can't do template message message
122    pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
123        let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
124
125        Self::CantDo(kind)
126    }
127
128    /// New DM message
129    pub fn new_dm(
130        id: Option<Uuid>,
131        request_id: Option<u64>,
132        action: Action,
133        payload: Option<Payload>,
134    ) -> Self {
135        let kind = MessageKind::new(id, request_id, None, action, payload);
136
137        Self::Dm(kind)
138    }
139
140    /// Get message from json string
141    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
142        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
143    }
144
145    /// Get message as json string
146    pub fn as_json(&self) -> Result<String, ServiceError> {
147        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
148    }
149
150    // Get inner message kind
151    pub fn get_inner_message_kind(&self) -> &MessageKind {
152        match self {
153            Message::Dispute(k)
154            | Message::Order(k)
155            | Message::CantDo(k)
156            | Message::Rate(k)
157            | Message::Dm(k) => k,
158        }
159    }
160
161    // Get action from the inner message
162    pub fn inner_action(&self) -> Option<Action> {
163        match self {
164            Message::Dispute(a)
165            | Message::Order(a)
166            | Message::CantDo(a)
167            | Message::Rate(a)
168            | Message::Dm(a) => Some(a.get_action()),
169        }
170    }
171
172    /// Verify if is valid the inner message
173    pub fn verify(&self) -> bool {
174        match self {
175            Message::Order(m)
176            | Message::Dispute(m)
177            | Message::CantDo(m)
178            | Message::Rate(m)
179            | Message::Dm(m) => m.verify(),
180        }
181    }
182
183    pub fn sign(message: String, keys: &Keys) -> Signature {
184        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
185        let hash = hash.to_byte_array();
186        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
187
188        keys.sign_schnorr(&message)
189    }
190
191    pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
192        // Create payload hash
193        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
194        let hash = hash.to_byte_array();
195        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
196
197        // Create a verification-only context for better performance
198        let secp = Secp256k1::verification_only();
199        // Verify signature
200        if let Ok(xonlykey) = pubkey.xonly() {
201            xonlykey.verify(&secp, &message, &sig).is_ok()
202        } else {
203            false
204        }
205    }
206}
207
208/// Use this Message to establish communication between users and Mostro
209#[derive(Debug, Clone, Deserialize, Serialize)]
210pub struct MessageKind {
211    /// Message version
212    pub version: u8,
213    /// Request_id for test on client
214    pub request_id: Option<u64>,
215    /// Trade key index
216    pub trade_index: Option<i64>,
217    /// Message id is not mandatory
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub id: Option<Uuid>,
220    /// Action to be taken
221    pub action: Action,
222    /// Payload of the Message
223    pub payload: Option<Payload>,
224}
225
226type Amount = i64;
227
228/// Payment failure retry information
229#[derive(Debug, Deserialize, Serialize, Clone)]
230pub struct PaymentFailedInfo {
231    /// Maximum number of payment attempts
232    pub payment_attempts: u32,
233    /// Retry interval in seconds between payment attempts
234    pub payment_retries_interval: u32,
235}
236
237/// Message payload
238#[derive(Debug, Deserialize, Serialize, Clone)]
239#[serde(rename_all = "snake_case")]
240pub enum Payload {
241    /// Order
242    Order(SmallOrder),
243    /// Payment request
244    PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
245    /// Use to send a message to another user
246    TextMessage(String),
247    /// Peer information
248    Peer(Peer),
249    /// Used to rate a user
250    RatingUser(u8),
251    /// In some cases we need to send an amount
252    Amount(Amount),
253    /// Dispute
254    Dispute(Uuid, Option<u16>, Option<SolverDisputeInfo>),
255    /// Here the reason why we can't do the action
256    CantDo(Option<CantDoReason>),
257    /// This is used by the maker of a range order only on
258    /// messages with action release and fiat-sent
259    /// to inform the next trade pubkey and trade index
260    NextTrade(String, u32),
261    /// Payment failure retry configuration information
262    PaymentFailed(PaymentFailedInfo),
263}
264
265#[allow(dead_code)]
266impl MessageKind {
267    /// New message
268    pub fn new(
269        id: Option<Uuid>,
270        request_id: Option<u64>,
271        trade_index: Option<i64>,
272        action: Action,
273        payload: Option<Payload>,
274    ) -> Self {
275        Self {
276            version: PROTOCOL_VER,
277            request_id,
278            trade_index,
279            id,
280            action,
281            payload,
282        }
283    }
284    /// Get message from json string
285    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
286        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
287    }
288    /// Get message as json string
289    pub fn as_json(&self) -> Result<String, ServiceError> {
290        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
291    }
292
293    // Get action from the inner message
294    pub fn get_action(&self) -> Action {
295        self.action.clone()
296    }
297
298    /// Get the next trade keys when order is settled
299    pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
300        match &self.payload {
301            Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
302            None => Ok(None),
303            _ => Err(ServiceError::InvalidPayload),
304        }
305    }
306
307    pub fn get_rating(&self) -> Result<u8, ServiceError> {
308        if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
309            if !(MIN_RATING..=MAX_RATING).contains(&v) {
310                return Err(ServiceError::InvalidRatingValue);
311            }
312            Ok(v)
313        } else {
314            Err(ServiceError::InvalidRating)
315        }
316    }
317
318    /// Verify if is valid message
319    pub fn verify(&self) -> bool {
320        match &self.action {
321            Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
322            Action::PayInvoice | Action::AddInvoice => {
323                if self.id.is_none() {
324                    return false;
325                }
326                matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
327            }
328            Action::TakeSell
329            | Action::TakeBuy
330            | Action::FiatSent
331            | Action::FiatSentOk
332            | Action::Release
333            | Action::Released
334            | Action::Dispute
335            | Action::AdminCancel
336            | Action::AdminCanceled
337            | Action::AdminSettle
338            | Action::AdminSettled
339            | Action::Rate
340            | Action::RateReceived
341            | Action::AdminTakeDispute
342            | Action::AdminTookDispute
343            | Action::DisputeInitiatedByYou
344            | Action::DisputeInitiatedByPeer
345            | Action::WaitingBuyerInvoice
346            | Action::PurchaseCompleted
347            | Action::HoldInvoicePaymentAccepted
348            | Action::HoldInvoicePaymentSettled
349            | Action::HoldInvoicePaymentCanceled
350            | Action::WaitingSellerToPay
351            | Action::BuyerTookOrder
352            | Action::BuyerInvoiceAccepted
353            | Action::CooperativeCancelInitiatedByYou
354            | Action::CooperativeCancelInitiatedByPeer
355            | Action::CooperativeCancelAccepted
356            | Action::Cancel
357            | Action::InvoiceUpdated
358            | Action::AdminAddSolver
359            | Action::SendDm
360            | Action::TradePubkey
361            | Action::Canceled => {
362                if self.id.is_none() {
363                    return false;
364                }
365                true
366            }
367            Action::PaymentFailed => {
368                if self.id.is_none() {
369                    return false;
370                }
371                matches!(&self.payload, Some(Payload::PaymentFailed(_)))
372            }
373            Action::RateUser => {
374                matches!(&self.payload, Some(Payload::RatingUser(_)))
375            }
376            Action::CantDo => {
377                matches!(&self.payload, Some(Payload::CantDo(_)))
378            }
379        }
380    }
381
382    pub fn get_order(&self) -> Option<&SmallOrder> {
383        if self.action != Action::NewOrder {
384            return None;
385        }
386        match &self.payload {
387            Some(Payload::Order(o)) => Some(o),
388            _ => None,
389        }
390    }
391
392    pub fn get_payment_request(&self) -> Option<String> {
393        if self.action != Action::TakeSell
394            && self.action != Action::AddInvoice
395            && self.action != Action::NewOrder
396        {
397            return None;
398        }
399        match &self.payload {
400            Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
401            Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
402            _ => None,
403        }
404    }
405
406    pub fn get_amount(&self) -> Option<Amount> {
407        if self.action != Action::TakeSell && self.action != Action::TakeBuy {
408            return None;
409        }
410        match &self.payload {
411            Some(Payload::PaymentRequest(_, _, amount)) => *amount,
412            Some(Payload::Amount(amount)) => Some(*amount),
413            _ => None,
414        }
415    }
416
417    pub fn get_payload(&self) -> Option<&Payload> {
418        self.payload.as_ref()
419    }
420
421    pub fn has_trade_index(&self) -> (bool, i64) {
422        if let Some(index) = self.trade_index {
423            return (true, index);
424        }
425        (false, 0)
426    }
427
428    pub fn trade_index(&self) -> i64 {
429        if let Some(index) = self.trade_index {
430            return index;
431        }
432        0
433    }
434}
435
436#[cfg(test)]
437mod test {
438    use crate::message::{Action, Message, MessageKind, Payload, Peer};
439    use crate::user::UserInfo;
440    use nostr_sdk::Keys;
441    use uuid::uuid;
442
443    #[test]
444    fn test_peer_with_reputation() {
445        // Test creating a Peer with reputation information
446        let reputation = UserInfo {
447            rating: 4.5,
448            reviews: 10,
449            operating_days: 30,
450        };
451        let peer = Peer::new(
452            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
453            Some(reputation.clone()),
454        );
455
456        // Assert the fields are set correctly
457        assert_eq!(
458            peer.pubkey,
459            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
460        );
461        assert!(peer.reputation.is_some());
462        let peer_reputation = peer.reputation.clone().unwrap();
463        assert_eq!(peer_reputation.rating, 4.5);
464        assert_eq!(peer_reputation.reviews, 10);
465        assert_eq!(peer_reputation.operating_days, 30);
466
467        // Test JSON serialization and deserialization
468        let json = peer.as_json().unwrap();
469        let deserialized_peer = Peer::from_json(&json).unwrap();
470        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
471        assert!(deserialized_peer.reputation.is_some());
472        let deserialized_reputation = deserialized_peer.reputation.unwrap();
473        assert_eq!(deserialized_reputation.rating, 4.5);
474        assert_eq!(deserialized_reputation.reviews, 10);
475        assert_eq!(deserialized_reputation.operating_days, 30);
476    }
477
478    #[test]
479    fn test_peer_without_reputation() {
480        // Test creating a Peer without reputation information
481        let peer = Peer::new(
482            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
483            None,
484        );
485
486        // Assert the reputation field is None
487        assert_eq!(
488            peer.pubkey,
489            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
490        );
491        assert!(peer.reputation.is_none());
492
493        // Test JSON serialization and deserialization
494        let json = peer.as_json().unwrap();
495        let deserialized_peer = Peer::from_json(&json).unwrap();
496        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
497        assert!(deserialized_peer.reputation.is_none());
498    }
499
500    #[test]
501    fn test_peer_in_message() {
502        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
503
504        // Test with reputation
505        let reputation = UserInfo {
506            rating: 4.5,
507            reviews: 10,
508            operating_days: 30,
509        };
510        let peer_with_reputation = Peer::new(
511            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
512            Some(reputation),
513        );
514        let payload_with_reputation = Payload::Peer(peer_with_reputation);
515        let message_with_reputation = Message::Order(MessageKind::new(
516            Some(uuid),
517            Some(1),
518            Some(2),
519            Action::FiatSentOk,
520            Some(payload_with_reputation),
521        ));
522
523        // Verify message with reputation
524        assert!(message_with_reputation.verify());
525        let message_json = message_with_reputation.as_json().unwrap();
526        let deserialized_message = Message::from_json(&message_json).unwrap();
527        assert!(deserialized_message.verify());
528
529        // Test without reputation
530        let peer_without_reputation = Peer::new(
531            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
532            None,
533        );
534        let payload_without_reputation = Payload::Peer(peer_without_reputation);
535        let message_without_reputation = Message::Order(MessageKind::new(
536            Some(uuid),
537            Some(1),
538            Some(2),
539            Action::FiatSentOk,
540            Some(payload_without_reputation),
541        ));
542
543        // Verify message without reputation
544        assert!(message_without_reputation.verify());
545        let message_json = message_without_reputation.as_json().unwrap();
546        let deserialized_message = Message::from_json(&message_json).unwrap();
547        assert!(deserialized_message.verify());
548    }
549
550    #[test]
551    fn test_payment_failed_payload() {
552        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
553        
554        // Test PaymentFailedInfo serialization and deserialization
555        let payment_failed_info = crate::message::PaymentFailedInfo {
556            payment_attempts: 3,
557            payment_retries_interval: 60,
558        };
559        
560        let payload = Payload::PaymentFailed(payment_failed_info);
561        let message = Message::Order(MessageKind::new(
562            Some(uuid),
563            Some(1),
564            Some(2),
565            Action::PaymentFailed,
566            Some(payload),
567        ));
568        
569        // Verify message validation
570        assert!(message.verify());
571        
572        // Test JSON serialization
573        let message_json = message.as_json().unwrap();
574        println!("PaymentFailed message JSON: {}", message_json);
575        
576        // Test deserialization
577        let deserialized_message = Message::from_json(&message_json).unwrap();
578        assert!(deserialized_message.verify());
579        
580        // Verify the payload contains correct values
581        if let Message::Order(kind) = deserialized_message {
582            if let Some(Payload::PaymentFailed(info)) = kind.payload {
583                assert_eq!(info.payment_attempts, 3);
584                assert_eq!(info.payment_retries_interval, 60);
585            } else {
586                panic!("Expected PaymentFailed payload");
587            }
588        } else {
589            panic!("Expected Order message");
590        }
591    }
592
593    #[test]
594    fn test_message_payload_signature() {
595        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
596        let peer = Peer::new(
597            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
598            None, // Add None for the reputation parameter
599        );
600        let payload = Payload::Peer(peer);
601        let test_message = Message::Order(MessageKind::new(
602            Some(uuid),
603            Some(1),
604            Some(2),
605            Action::FiatSentOk,
606            Some(payload),
607        ));
608        assert!(test_message.verify());
609        let test_message_json = test_message.as_json().unwrap();
610        // Message should be signed with the trade keys
611        let trade_keys =
612            Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
613                .unwrap();
614        let sig = Message::sign(test_message_json.clone(), &trade_keys);
615
616        assert!(Message::verify_signature(
617            test_message_json,
618            trade_keys.public_key(),
619            sig
620        ));
621    }
622}