mostro_core/
message.rs

1use crate::dispute::SolverDisputeInfo;
2use crate::error::ServiceError;
3use crate::PROTOCOL_VER;
4use crate::{error::CantDoReason, order::SmallOrder};
5use anyhow::Result;
6use bitcoin::hashes::sha256::Hash as Sha256Hash;
7use bitcoin::hashes::Hash;
8use bitcoin::key::Secp256k1;
9use bitcoin::secp256k1::Message as BitcoinMessage;
10use nostr_sdk::prelude::*;
11use serde::{Deserialize, Serialize};
12use std::fmt;
13use uuid::Uuid;
14
15// Max rating
16pub const MAX_RATING: u8 = 5;
17// Min rating
18pub const MIN_RATING: u8 = 1;
19
20/// One party of the trade
21#[derive(Debug, Deserialize, Serialize, Clone)]
22pub struct Peer {
23    pub pubkey: String,
24}
25
26impl Peer {
27    pub fn new(pubkey: String) -> Self {
28        Self { pubkey }
29    }
30
31    pub fn from_json(json: &str) -> Result<Self> {
32        Ok(serde_json::from_str(json)?)
33    }
34
35    pub fn as_json(&self) -> Result<String> {
36        Ok(serde_json::to_string(&self)?)
37    }
38}
39
40/// Action is used to identify each message between Mostro and users
41#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
42#[serde(rename_all = "kebab-case")]
43pub enum Action {
44    NewOrder,
45    TakeSell,
46    TakeBuy,
47    PayInvoice,
48    FiatSent,
49    FiatSentOk,
50    Release,
51    Released,
52    Cancel,
53    Canceled,
54    CooperativeCancelInitiatedByYou,
55    CooperativeCancelInitiatedByPeer,
56    DisputeInitiatedByYou,
57    DisputeInitiatedByPeer,
58    CooperativeCancelAccepted,
59    BuyerInvoiceAccepted,
60    PurchaseCompleted,
61    HoldInvoicePaymentAccepted,
62    HoldInvoicePaymentSettled,
63    HoldInvoicePaymentCanceled,
64    WaitingSellerToPay,
65    WaitingBuyerInvoice,
66    AddInvoice,
67    BuyerTookOrder,
68    Rate,
69    RateUser,
70    RateReceived,
71    CantDo,
72    Dispute,
73    AdminCancel,
74    AdminCanceled,
75    AdminSettle,
76    AdminSettled,
77    AdminAddSolver,
78    AdminTakeDispute,
79    AdminTookDispute,
80    PaymentFailed,
81    InvoiceUpdated,
82    SendDm,
83    TradePubkey,
84}
85
86impl fmt::Display for Action {
87    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
88        write!(f, "{self:?}")
89    }
90}
91
92/// Use this Message to establish communication between users and Mostro
93#[derive(Debug, Clone, Deserialize, Serialize)]
94#[serde(rename_all = "kebab-case")]
95pub enum Message {
96    Order(MessageKind),
97    Dispute(MessageKind),
98    CantDo(MessageKind),
99    Rate(MessageKind),
100    Dm(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    /// New can't do template message message
130    pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
131        let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
132
133        Self::CantDo(kind)
134    }
135
136    /// New DM message
137    pub fn new_dm(
138        id: Option<Uuid>,
139        request_id: Option<u64>,
140        action: Action,
141        payload: Option<Payload>,
142    ) -> Self {
143        let kind = MessageKind::new(id, request_id, None, action, payload);
144
145        Self::Dm(kind)
146    }
147
148    /// Get message from json string
149    pub fn from_json(json: &str) -> Result<Self> {
150        Ok(serde_json::from_str(json)?)
151    }
152
153    /// Get message as json string
154    pub fn as_json(&self) -> Result<String> {
155        Ok(serde_json::to_string(&self)?)
156    }
157
158    // Get inner message kind
159    pub fn get_inner_message_kind(&self) -> &MessageKind {
160        match self {
161            Message::Dispute(k)
162            | Message::Order(k)
163            | Message::CantDo(k)
164            | Message::Rate(k)
165            | Message::Dm(k) => k,
166        }
167    }
168
169    // Get action from the inner message
170    pub fn inner_action(&self) -> Option<Action> {
171        match self {
172            Message::Dispute(a)
173            | Message::Order(a)
174            | Message::CantDo(a)
175            | Message::Rate(a)
176            | Message::Dm(a) => Some(a.get_action()),
177        }
178    }
179
180    /// Verify if is valid the inner message
181    pub fn verify(&self) -> bool {
182        match self {
183            Message::Order(m)
184            | Message::Dispute(m)
185            | Message::CantDo(m)
186            | Message::Rate(m)
187            | Message::Dm(m) => m.verify(),
188        }
189    }
190
191    pub fn sign(message: String, keys: &Keys) -> Signature {
192        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
193        let hash = hash.to_byte_array();
194        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
195
196        keys.sign_schnorr(&message)
197    }
198
199    pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
200        // Create payload hash
201        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
202        let hash = hash.to_byte_array();
203        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
204        // Create a verification-only context for better performance
205        let secp = Secp256k1::verification_only();
206        // Verify signature
207        pubkey.verify(&secp, &message, &sig).is_ok()
208    }
209}
210
211/// Use this Message to establish communication between users and Mostro
212#[derive(Debug, Clone, Deserialize, Serialize)]
213pub struct MessageKind {
214    /// Message version
215    pub version: u8,
216    /// Request_id for test on client
217    pub request_id: Option<u64>,
218    /// Trade key index
219    pub trade_index: Option<i64>,
220    /// Message id is not mandatory
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub id: Option<Uuid>,
223    /// Action to be taken
224    pub action: Action,
225    /// Payload of the Message
226    pub payload: Option<Payload>,
227}
228
229type Amount = i64;
230
231/// Message payload
232#[derive(Debug, Deserialize, Serialize, Clone)]
233#[serde(rename_all = "snake_case")]
234pub enum Payload {
235    /// Order
236    Order(SmallOrder),
237    /// Payment request
238    PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
239    /// Use to send a message to another user
240    TextMessage(String),
241    /// Peer information
242    Peer(Peer),
243    /// Used to rate a user
244    RatingUser(u8),
245    /// In some cases we need to send an amount
246    Amount(Amount),
247    /// Dispute
248    Dispute(Uuid, Option<u16>, Option<SolverDisputeInfo>),
249    /// Here the reason why we can't do the action
250    CantDo(Option<CantDoReason>),
251    /// This is used by the maker of a range order only on
252    /// messages with action release and fiat-sent
253    /// to inform the next trade pubkey and trade index
254    NextTrade(String, u32),
255}
256
257#[allow(dead_code)]
258impl MessageKind {
259    /// New message
260    pub fn new(
261        id: Option<Uuid>,
262        request_id: Option<u64>,
263        trade_index: Option<i64>,
264        action: Action,
265        payload: Option<Payload>,
266    ) -> Self {
267        Self {
268            version: PROTOCOL_VER,
269            request_id,
270            trade_index,
271            id,
272            action,
273            payload,
274        }
275    }
276    /// Get message from json string
277    pub fn from_json(json: &str) -> Result<Self> {
278        Ok(serde_json::from_str(json)?)
279    }
280    /// Get message as json string
281    pub fn as_json(&self) -> Result<String> {
282        Ok(serde_json::to_string(&self)?)
283    }
284
285    // Get action from the inner message
286    pub fn get_action(&self) -> Action {
287        self.action.clone()
288    }
289
290    /// Get the next trade keys when order is settled
291    pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
292        match &self.payload {
293            Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
294            None => Ok(None),
295            _ => Err(ServiceError::InvalidPayload),
296        }
297    }
298
299    pub fn get_rating(&self) -> Result<u8, ServiceError> {
300        if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
301            if !(MIN_RATING..=MAX_RATING).contains(&v) {
302                return Err(ServiceError::InvalidRatingValue);
303            }
304            Ok(v)
305        } else {
306            Err(ServiceError::InvalidRating)
307        }
308    }
309
310    /// Verify if is valid message
311    pub fn verify(&self) -> bool {
312        match &self.action {
313            Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
314            Action::PayInvoice | Action::AddInvoice => {
315                if self.id.is_none() {
316                    return false;
317                }
318                matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
319            }
320            Action::TakeSell
321            | Action::TakeBuy
322            | Action::FiatSent
323            | Action::FiatSentOk
324            | Action::Release
325            | Action::Released
326            | Action::Dispute
327            | Action::AdminCancel
328            | Action::AdminCanceled
329            | Action::AdminSettle
330            | Action::AdminSettled
331            | Action::Rate
332            | Action::RateReceived
333            | Action::AdminTakeDispute
334            | Action::AdminTookDispute
335            | Action::DisputeInitiatedByYou
336            | Action::DisputeInitiatedByPeer
337            | Action::WaitingBuyerInvoice
338            | Action::PurchaseCompleted
339            | Action::HoldInvoicePaymentAccepted
340            | Action::HoldInvoicePaymentSettled
341            | Action::HoldInvoicePaymentCanceled
342            | Action::WaitingSellerToPay
343            | Action::BuyerTookOrder
344            | Action::BuyerInvoiceAccepted
345            | Action::CooperativeCancelInitiatedByYou
346            | Action::CooperativeCancelInitiatedByPeer
347            | Action::CooperativeCancelAccepted
348            | Action::Cancel
349            | Action::PaymentFailed
350            | Action::InvoiceUpdated
351            | Action::AdminAddSolver
352            | Action::SendDm
353            | Action::TradePubkey
354            | Action::Canceled => {
355                if self.id.is_none() {
356                    return false;
357                }
358                true
359            }
360            Action::RateUser => {
361                matches!(&self.payload, Some(Payload::RatingUser(_)))
362            }
363            Action::CantDo => {
364                matches!(&self.payload, Some(Payload::CantDo(_)))
365            }
366        }
367    }
368
369    pub fn get_order(&self) -> Option<&SmallOrder> {
370        if self.action != Action::NewOrder {
371            return None;
372        }
373        match &self.payload {
374            Some(Payload::Order(o)) => Some(o),
375            _ => None,
376        }
377    }
378
379    pub fn get_payment_request(&self) -> Option<String> {
380        if self.action != Action::TakeSell
381            && self.action != Action::AddInvoice
382            && self.action != Action::NewOrder
383        {
384            return None;
385        }
386        match &self.payload {
387            Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
388            Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
389            _ => None,
390        }
391    }
392
393    pub fn get_amount(&self) -> Option<Amount> {
394        if self.action != Action::TakeSell && self.action != Action::TakeBuy {
395            return None;
396        }
397        match &self.payload {
398            Some(Payload::PaymentRequest(_, _, amount)) => *amount,
399            Some(Payload::Amount(amount)) => Some(*amount),
400            _ => None,
401        }
402    }
403
404    pub fn get_payload(&self) -> Option<&Payload> {
405        self.payload.as_ref()
406    }
407
408    pub fn has_trade_index(&self) -> (bool, i64) {
409        if let Some(index) = self.trade_index {
410            return (true, index);
411        }
412        (false, 0)
413    }
414
415    pub fn trade_index(&self) -> i64 {
416        if let Some(index) = self.trade_index {
417            return index;
418        }
419        0
420    }
421}