mostro_core/
message.rs

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