mostro_core/
message.rs

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