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/// Message payload
229#[derive(Debug, Deserialize, Serialize, Clone)]
230#[serde(rename_all = "snake_case")]
231pub enum Payload {
232    /// Order
233    Order(SmallOrder),
234    /// Payment request
235    PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
236    /// Use to send a message to another user
237    TextMessage(String),
238    /// Peer information
239    Peer(Peer),
240    /// Used to rate a user
241    RatingUser(u8),
242    /// In some cases we need to send an amount
243    Amount(Amount),
244    /// Dispute
245    Dispute(Uuid, Option<u16>, Option<SolverDisputeInfo>),
246    /// Here the reason why we can't do the action
247    CantDo(Option<CantDoReason>),
248    /// This is used by the maker of a range order only on
249    /// messages with action release and fiat-sent
250    /// to inform the next trade pubkey and trade index
251    NextTrade(String, u32),
252}
253
254#[allow(dead_code)]
255impl MessageKind {
256    /// New message
257    pub fn new(
258        id: Option<Uuid>,
259        request_id: Option<u64>,
260        trade_index: Option<i64>,
261        action: Action,
262        payload: Option<Payload>,
263    ) -> Self {
264        Self {
265            version: PROTOCOL_VER,
266            request_id,
267            trade_index,
268            id,
269            action,
270            payload,
271        }
272    }
273    /// Get message from json string
274    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
275        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
276    }
277    /// Get message as json string
278    pub fn as_json(&self) -> Result<String, ServiceError> {
279        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
280    }
281
282    // Get action from the inner message
283    pub fn get_action(&self) -> Action {
284        self.action.clone()
285    }
286
287    /// Get the next trade keys when order is settled
288    pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
289        match &self.payload {
290            Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
291            None => Ok(None),
292            _ => Err(ServiceError::InvalidPayload),
293        }
294    }
295
296    pub fn get_rating(&self) -> Result<u8, ServiceError> {
297        if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
298            if !(MIN_RATING..=MAX_RATING).contains(&v) {
299                return Err(ServiceError::InvalidRatingValue);
300            }
301            Ok(v)
302        } else {
303            Err(ServiceError::InvalidRating)
304        }
305    }
306
307    /// Verify if is valid message
308    pub fn verify(&self) -> bool {
309        match &self.action {
310            Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
311            Action::PayInvoice | Action::AddInvoice => {
312                if self.id.is_none() {
313                    return false;
314                }
315                matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
316            }
317            Action::TakeSell
318            | Action::TakeBuy
319            | Action::FiatSent
320            | Action::FiatSentOk
321            | Action::Release
322            | Action::Released
323            | Action::Dispute
324            | Action::AdminCancel
325            | Action::AdminCanceled
326            | Action::AdminSettle
327            | Action::AdminSettled
328            | Action::Rate
329            | Action::RateReceived
330            | Action::AdminTakeDispute
331            | Action::AdminTookDispute
332            | Action::DisputeInitiatedByYou
333            | Action::DisputeInitiatedByPeer
334            | Action::WaitingBuyerInvoice
335            | Action::PurchaseCompleted
336            | Action::HoldInvoicePaymentAccepted
337            | Action::HoldInvoicePaymentSettled
338            | Action::HoldInvoicePaymentCanceled
339            | Action::WaitingSellerToPay
340            | Action::BuyerTookOrder
341            | Action::BuyerInvoiceAccepted
342            | Action::CooperativeCancelInitiatedByYou
343            | Action::CooperativeCancelInitiatedByPeer
344            | Action::CooperativeCancelAccepted
345            | Action::Cancel
346            | Action::PaymentFailed
347            | Action::InvoiceUpdated
348            | Action::AdminAddSolver
349            | Action::SendDm
350            | Action::TradePubkey
351            | Action::Canceled => {
352                if self.id.is_none() {
353                    return false;
354                }
355                true
356            }
357            Action::RateUser => {
358                matches!(&self.payload, Some(Payload::RatingUser(_)))
359            }
360            Action::CantDo => {
361                matches!(&self.payload, Some(Payload::CantDo(_)))
362            }
363        }
364    }
365
366    pub fn get_order(&self) -> Option<&SmallOrder> {
367        if self.action != Action::NewOrder {
368            return None;
369        }
370        match &self.payload {
371            Some(Payload::Order(o)) => Some(o),
372            _ => None,
373        }
374    }
375
376    pub fn get_payment_request(&self) -> Option<String> {
377        if self.action != Action::TakeSell
378            && self.action != Action::AddInvoice
379            && self.action != Action::NewOrder
380        {
381            return None;
382        }
383        match &self.payload {
384            Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
385            Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
386            _ => None,
387        }
388    }
389
390    pub fn get_amount(&self) -> Option<Amount> {
391        if self.action != Action::TakeSell && self.action != Action::TakeBuy {
392            return None;
393        }
394        match &self.payload {
395            Some(Payload::PaymentRequest(_, _, amount)) => *amount,
396            Some(Payload::Amount(amount)) => Some(*amount),
397            _ => None,
398        }
399    }
400
401    pub fn get_payload(&self) -> Option<&Payload> {
402        self.payload.as_ref()
403    }
404
405    pub fn has_trade_index(&self) -> (bool, i64) {
406        if let Some(index) = self.trade_index {
407            return (true, index);
408        }
409        (false, 0)
410    }
411
412    pub fn trade_index(&self) -> i64 {
413        if let Some(index) = self.trade_index {
414            return index;
415        }
416        0
417    }
418}
419
420#[cfg(test)]
421mod test {
422    use crate::message::{Action, Message, MessageKind, Payload, Peer};
423    use crate::user::UserInfo;
424    use nostr_sdk::Keys;
425    use uuid::uuid;
426
427    #[test]
428    fn test_peer_with_reputation() {
429        // Test creating a Peer with reputation information
430        let reputation = UserInfo {
431            rating: 4.5,
432            reviews: 10,
433            operating_days: 30,
434        };
435        let peer = Peer::new(
436            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
437            Some(reputation.clone()),
438        );
439
440        // Assert the fields are set correctly
441        assert_eq!(
442            peer.pubkey,
443            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
444        );
445        assert!(peer.reputation.is_some());
446        let peer_reputation = peer.reputation.clone().unwrap();
447        assert_eq!(peer_reputation.rating, 4.5);
448        assert_eq!(peer_reputation.reviews, 10);
449        assert_eq!(peer_reputation.operating_days, 30);
450
451        // Test JSON serialization and deserialization
452        let json = peer.as_json().unwrap();
453        let deserialized_peer = Peer::from_json(&json).unwrap();
454        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
455        assert!(deserialized_peer.reputation.is_some());
456        let deserialized_reputation = deserialized_peer.reputation.unwrap();
457        assert_eq!(deserialized_reputation.rating, 4.5);
458        assert_eq!(deserialized_reputation.reviews, 10);
459        assert_eq!(deserialized_reputation.operating_days, 30);
460    }
461
462    #[test]
463    fn test_peer_without_reputation() {
464        // Test creating a Peer without reputation information
465        let peer = Peer::new(
466            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
467            None,
468        );
469
470        // Assert the reputation field is None
471        assert_eq!(
472            peer.pubkey,
473            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
474        );
475        assert!(peer.reputation.is_none());
476
477        // Test JSON serialization and deserialization
478        let json = peer.as_json().unwrap();
479        let deserialized_peer = Peer::from_json(&json).unwrap();
480        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
481        assert!(deserialized_peer.reputation.is_none());
482    }
483
484    #[test]
485    fn test_peer_in_message() {
486        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
487
488        // Test with reputation
489        let reputation = UserInfo {
490            rating: 4.5,
491            reviews: 10,
492            operating_days: 30,
493        };
494        let peer_with_reputation = Peer::new(
495            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
496            Some(reputation),
497        );
498        let payload_with_reputation = Payload::Peer(peer_with_reputation);
499        let message_with_reputation = Message::Order(MessageKind::new(
500            Some(uuid),
501            Some(1),
502            Some(2),
503            Action::FiatSentOk,
504            Some(payload_with_reputation),
505        ));
506
507        // Verify message with reputation
508        assert!(message_with_reputation.verify());
509        let message_json = message_with_reputation.as_json().unwrap();
510        let deserialized_message = Message::from_json(&message_json).unwrap();
511        assert!(deserialized_message.verify());
512
513        // Test without reputation
514        let peer_without_reputation = Peer::new(
515            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
516            None,
517        );
518        let payload_without_reputation = Payload::Peer(peer_without_reputation);
519        let message_without_reputation = Message::Order(MessageKind::new(
520            Some(uuid),
521            Some(1),
522            Some(2),
523            Action::FiatSentOk,
524            Some(payload_without_reputation),
525        ));
526
527        // Verify message without reputation
528        assert!(message_without_reputation.verify());
529        let message_json = message_without_reputation.as_json().unwrap();
530        let deserialized_message = Message::from_json(&message_json).unwrap();
531        assert!(deserialized_message.verify());
532    }
533
534    #[test]
535    fn test_message_payload_signature() {
536        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
537        let peer = Peer::new(
538            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
539            None, // Add None for the reputation parameter
540        );
541        let payload = Payload::Peer(peer);
542        let test_message = Message::Order(MessageKind::new(
543            Some(uuid),
544            Some(1),
545            Some(2),
546            Action::FiatSentOk,
547            Some(payload),
548        ));
549        assert!(test_message.verify());
550        let test_message_json = test_message.as_json().unwrap();
551        // Message should be signed with the trade keys
552        let trade_keys =
553            Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
554                .unwrap();
555        let sig = Message::sign(test_message_json.clone(), &trade_keys);
556
557        assert!(Message::verify_signature(
558            test_message_json,
559            trade_keys.public_key(),
560            sig
561        ));
562    }
563}