Skip to main content

mostro_core/
message.rs

1//! Protocol message envelope exchanged between clients and a Mostro node.
2//!
3//! The top-level type is [`Message`], a tagged union that carries a
4//! [`MessageKind`] together with a discriminator (order, dispute, DM, rate,
5//! can't-do, restore). [`MessageKind`] holds the shared fields present on
6//! every request/response: protocol version, optional identifier, trade
7//! index, [`Action`] and [`Payload`].
8//!
9//! In transit, messages are serialized to JSON, optionally signed with the
10//! sender's trade keys using [`Message::sign`], and wrapped in a NIP-59
11//! envelope by [`crate::nip59::wrap_message`].
12
13use crate::prelude::*;
14use bitcoin::hashes::sha256::Hash as Sha256Hash;
15use bitcoin::hashes::Hash;
16use bitcoin::key::Secp256k1;
17use bitcoin::secp256k1::Message as BitcoinMessage;
18use nostr_sdk::prelude::*;
19#[cfg(feature = "sqlx")]
20use sqlx::FromRow;
21#[cfg(feature = "sqlx")]
22use sqlx_crud::SqlxCrud;
23
24use std::fmt;
25use uuid::Uuid;
26
27/// Identity of a counterpart in a trade.
28///
29/// `Peer` bundles the counterpart's trade public key with an optional
30/// [`UserInfo`] snapshot so it can be embedded into messages that need to
31/// surface reputation (for example the peer disclosure sent with
32/// [`Action::FiatSentOk`]).
33#[derive(Debug, Deserialize, Serialize, Clone)]
34pub struct Peer {
35    /// Trade public key of the peer (hex or npub).
36    pub pubkey: String,
37    /// Optional reputation snapshot. Absent when the peer operates in full
38    /// privacy mode.
39    pub reputation: Option<UserInfo>,
40}
41
42impl Peer {
43    /// Create a new [`Peer`].
44    pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
45        Self { pubkey, reputation }
46    }
47
48    /// Parse a [`Peer`] from its JSON representation.
49    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
50        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
51    }
52
53    /// Serialize the peer to a JSON string.
54    pub fn as_json(&self) -> Result<String, ServiceError> {
55        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
56    }
57}
58
59/// Discriminator describing the verb of a Mostro message.
60///
61/// `Action` values are serialized in `kebab-case`. Each action has its own
62/// expected [`Payload`] shape — see [`MessageKind::verify`] for the full
63/// matrix.
64#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
65#[serde(rename_all = "kebab-case")]
66pub enum Action {
67    /// Publish a new order. Payload: [`Payload::Order`].
68    NewOrder,
69    /// Take an existing `sell` order. Payload: optional
70    /// [`Payload::PaymentRequest`] or [`Payload::Amount`].
71    TakeSell,
72    /// Take an existing `buy` order. Payload: optional [`Payload::Amount`].
73    TakeBuy,
74    /// Request the taker to pay a Lightning invoice.
75    /// Payload: [`Payload::PaymentRequest`].
76    PayInvoice,
77    /// Buyer notifies Mostro that fiat was sent.
78    FiatSent,
79    /// Mostro acknowledges the fiat-sent notification to the seller.
80    FiatSentOk,
81    /// Seller releases the hold invoice funds.
82    Release,
83    /// Mostro confirms that the funds have been released.
84    Released,
85    /// Cancel an order.
86    Cancel,
87    /// Mostro confirms that the order was canceled.
88    Canceled,
89    /// Local side started a cooperative cancel.
90    CooperativeCancelInitiatedByYou,
91    /// Remote side started a cooperative cancel.
92    CooperativeCancelInitiatedByPeer,
93    /// Local side opened a dispute.
94    DisputeInitiatedByYou,
95    /// Remote side opened a dispute.
96    DisputeInitiatedByPeer,
97    /// Both sides agreed on the cooperative cancel.
98    CooperativeCancelAccepted,
99    /// Mostro accepted the buyer's payout invoice.
100    BuyerInvoiceAccepted,
101    /// Trade completed successfully.
102    PurchaseCompleted,
103    /// Mostro saw the hold-invoice payment accepted by the node.
104    HoldInvoicePaymentAccepted,
105    /// Mostro saw the hold-invoice payment settled.
106    HoldInvoicePaymentSettled,
107    /// Mostro saw the hold-invoice payment canceled.
108    HoldInvoicePaymentCanceled,
109    /// Informational: waiting for the seller to pay the hold invoice.
110    WaitingSellerToPay,
111    /// Informational: waiting for the buyer's payout invoice.
112    WaitingBuyerInvoice,
113    /// Buyer sends/updates its payout invoice.
114    /// Payload: [`Payload::PaymentRequest`].
115    AddInvoice,
116    /// Informational: a buyer has taken a sell order.
117    BuyerTookOrder,
118    /// Server-initiated rating request.
119    Rate,
120    /// Client-initiated rate. Payload: [`Payload::RatingUser`].
121    RateUser,
122    /// Acknowledgement of a received rating.
123    RateReceived,
124    /// Mostro returns a structured refusal. Payload: [`Payload::CantDo`].
125    CantDo,
126    /// Client-initiated dispute.
127    Dispute,
128    /// Admin cancels a trade.
129    AdminCancel,
130    /// Admin cancel acknowledged.
131    AdminCanceled,
132    /// Admin settles the hold invoice.
133    AdminSettle,
134    /// Admin settle acknowledged.
135    AdminSettled,
136    /// Admin registers a new dispute solver.
137    AdminAddSolver,
138    /// Solver takes a dispute.
139    AdminTakeDispute,
140    /// Solver took the dispute acknowledged.
141    AdminTookDispute,
142    /// Notification that a Lightning payment failed.
143    /// Payload: [`Payload::PaymentFailed`].
144    PaymentFailed,
145    /// Invoice associated with the order was updated.
146    InvoiceUpdated,
147    /// Direct message between users. Payload: [`Payload::TextMessage`].
148    SendDm,
149    /// Disclosure of a counterpart's trade pubkey. Payload: [`Payload::Peer`].
150    TradePubkey,
151    /// Client asks Mostro to restore its session state. Payload must be `None`.
152    RestoreSession,
153    /// Client asks Mostro for its last known trade index. Payload must be
154    /// `None`.
155    LastTradeIndex,
156    /// Listing of orders in response to a query.
157    /// Payload: [`Payload::Ids`] or [`Payload::Orders`].
158    Orders,
159}
160
161impl fmt::Display for Action {
162    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
163        write!(f, "{self:?}")
164    }
165}
166
167/// Top-level Mostro message exchanged between users and Mostro.
168///
169/// `Message` is a tagged union: every variant carries the shared
170/// [`MessageKind`] body, while the variant itself tells the receiver which
171/// channel the message belongs to (orders, disputes, DMs, rating, can't-do,
172/// session restore). Serializes as `kebab-case` JSON.
173#[derive(Debug, Clone, Deserialize, Serialize)]
174#[serde(rename_all = "kebab-case")]
175pub enum Message {
176    /// Order-channel message.
177    Order(MessageKind),
178    /// Dispute-channel message.
179    Dispute(MessageKind),
180    /// "Can't do" response returned by the Mostro node.
181    CantDo(MessageKind),
182    /// Rating message (server-initiated rate request or client rate).
183    Rate(MessageKind),
184    /// Direct message between users.
185    Dm(MessageKind),
186    /// Session restore request/response.
187    Restore(MessageKind),
188}
189
190impl Message {
191    /// Build a new `Message::Order` wrapping a freshly constructed
192    /// [`MessageKind`].
193    pub fn new_order(
194        id: Option<Uuid>,
195        request_id: Option<u64>,
196        trade_index: Option<i64>,
197        action: Action,
198        payload: Option<Payload>,
199    ) -> Self {
200        let kind = MessageKind::new(id, request_id, trade_index, action, payload);
201        Self::Order(kind)
202    }
203
204    /// Build a new `Message::Dispute` wrapping a freshly constructed
205    /// [`MessageKind`].
206    pub fn new_dispute(
207        id: Option<Uuid>,
208        request_id: Option<u64>,
209        trade_index: Option<i64>,
210        action: Action,
211        payload: Option<Payload>,
212    ) -> Self {
213        let kind = MessageKind::new(id, request_id, trade_index, action, payload);
214
215        Self::Dispute(kind)
216    }
217
218    /// Build a new `Message::Restore` with [`Action::RestoreSession`].
219    ///
220    /// According to [`MessageKind::verify`], the payload for a restore
221    /// request must be `None`. Any other payload yields an invalid message.
222    pub fn new_restore(payload: Option<Payload>) -> Self {
223        let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
224        Self::Restore(kind)
225    }
226
227    /// Build a new `Message::CantDo` message (a structured refusal sent by
228    /// Mostro when a request cannot be fulfilled).
229    pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
230        let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
231
232        Self::CantDo(kind)
233    }
234
235    /// Build a new `Message::Dm` carrying a direct message between users.
236    pub fn new_dm(
237        id: Option<Uuid>,
238        request_id: Option<u64>,
239        action: Action,
240        payload: Option<Payload>,
241    ) -> Self {
242        let kind = MessageKind::new(id, request_id, None, action, payload);
243
244        Self::Dm(kind)
245    }
246
247    /// Parse a [`Message`] from its JSON representation.
248    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
249        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
250    }
251
252    /// Serialize the message to a JSON string.
253    pub fn as_json(&self) -> Result<String, ServiceError> {
254        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
255    }
256
257    /// Borrow the inner [`MessageKind`] regardless of the variant.
258    pub fn get_inner_message_kind(&self) -> &MessageKind {
259        match self {
260            Message::Dispute(k)
261            | Message::Order(k)
262            | Message::CantDo(k)
263            | Message::Rate(k)
264            | Message::Dm(k)
265            | Message::Restore(k) => k,
266        }
267    }
268
269    /// Return the [`Action`] of the inner [`MessageKind`].
270    ///
271    /// Always returns `Some` for the current variant set; the `Option` is
272    /// kept for API stability.
273    pub fn inner_action(&self) -> Option<Action> {
274        match self {
275            Message::Dispute(a)
276            | Message::Order(a)
277            | Message::CantDo(a)
278            | Message::Rate(a)
279            | Message::Dm(a)
280            | Message::Restore(a) => Some(a.get_action()),
281        }
282    }
283
284    /// Validate that the inner [`MessageKind`] is consistent with its
285    /// [`Action`]. Delegates to [`MessageKind::verify`].
286    pub fn verify(&self) -> bool {
287        match self {
288            Message::Order(m)
289            | Message::Dispute(m)
290            | Message::CantDo(m)
291            | Message::Rate(m)
292            | Message::Dm(m)
293            | Message::Restore(m) => m.verify(),
294        }
295    }
296
297    /// Produce a Schnorr signature over the SHA-256 digest of `message`
298    /// using `keys`.
299    ///
300    /// This is the signature embedded in the rumor tuple when
301    /// [`crate::nip59::wrap_message`] is called with
302    /// [`WrapOptions::signed`](crate::nip59::WrapOptions::signed) set to
303    /// `true`. It binds a message to the sender's trade keys without
304    /// relying on the outer Nostr event signature.
305    pub fn sign(message: String, keys: &Keys) -> Signature {
306        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
307        let hash = hash.to_byte_array();
308        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
309
310        keys.sign_schnorr(&message)
311    }
312
313    /// Verify a signature previously produced by [`Message::sign`].
314    ///
315    /// Returns `true` when `sig` is a valid Schnorr signature of the
316    /// SHA-256 digest of `message` under `pubkey`, `false` otherwise
317    /// (including when `pubkey` has no x-only representation).
318    pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
319        // Create payload hash
320        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
321        let hash = hash.to_byte_array();
322        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
323
324        // Create a verification-only context for better performance
325        let secp = Secp256k1::verification_only();
326        // Verify signature
327        if let Ok(xonlykey) = pubkey.xonly() {
328            xonlykey.verify(&secp, &message, &sig).is_ok()
329        } else {
330            false
331        }
332    }
333}
334
335/// Body shared by every [`Message`] variant.
336///
337/// All Mostro protocol messages share this envelope: a protocol version,
338/// an optional client-chosen request id for correlation, a trade index used
339/// to enforce strictly increasing sequences per user, an optional
340/// order/dispute id, an [`Action`] and an optional [`Payload`].
341#[derive(Debug, Clone, Deserialize, Serialize)]
342pub struct MessageKind {
343    /// Mostro protocol version. Set to
344    /// `PROTOCOL_VER` by [`MessageKind::new`].
345    pub version: u8,
346    /// Client-chosen correlation id, echoed back on responses so the client
347    /// can match them to in-flight requests.
348    pub request_id: Option<u64>,
349    /// Trade index attached to this message. Must be strictly greater than
350    /// the last trade index Mostro has seen for the sender.
351    pub trade_index: Option<i64>,
352    /// Optional target identifier (usually the id of an [`crate::order::Order`]
353    /// or [`crate::dispute::Dispute`]).
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub id: Option<Uuid>,
356    /// Verb of the message.
357    pub action: Action,
358    /// Payload attached to the action. The allowed shape for a given action
359    /// is enforced by [`MessageKind::verify`].
360    pub payload: Option<Payload>,
361}
362
363/// Alias for a signed integer amount in satoshis.
364type Amount = i64;
365
366/// Retry configuration for a failed Lightning payment.
367///
368/// Sent inside a [`Payload::PaymentFailed`] so the client knows how many
369/// retries to expect and how long to wait between them.
370#[derive(Debug, Deserialize, Serialize, Clone)]
371pub struct PaymentFailedInfo {
372    /// Maximum number of payment attempts Mostro will perform.
373    pub payment_attempts: u32,
374    /// Delay in seconds between two retry attempts.
375    pub payment_retries_interval: u32,
376}
377
378/// Row-mapper used by `mostrod` when fetching metadata for session restore.
379///
380/// Not intended as a general-purpose order representation — field names are
381/// chosen to match the SQL `SELECT` aliases used by the server query.
382#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
383#[derive(Debug, Deserialize, Serialize, Clone)]
384pub struct RestoredOrderHelper {
385    /// Order id.
386    pub id: Uuid,
387    /// Order status, serialized as kebab-case.
388    pub status: String,
389    /// Master identity pubkey of the buyer, if any.
390    pub master_buyer_pubkey: Option<String>,
391    /// Master identity pubkey of the seller, if any.
392    pub master_seller_pubkey: Option<String>,
393    /// Trade index the buyer used on this order.
394    pub trade_index_buyer: Option<i64>,
395    /// Trade index the seller used on this order.
396    pub trade_index_seller: Option<i64>,
397}
398
399/// Row-mapper used by `mostrod` when fetching disputes for session restore.
400///
401/// Field names are chosen to match the SQL `SELECT` aliases in the restore
402/// query (in particular `status` is aliased as `dispute_status`).
403#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
404#[derive(Debug, Deserialize, Serialize, Clone)]
405pub struct RestoredDisputeHelper {
406    /// Dispute id.
407    pub dispute_id: Uuid,
408    /// Order id the dispute is attached to.
409    pub order_id: Uuid,
410    /// Dispute status, serialized as kebab-case.
411    pub dispute_status: String,
412    /// Master identity pubkey of the buyer, if any.
413    pub master_buyer_pubkey: Option<String>,
414    /// Master identity pubkey of the seller, if any.
415    pub master_seller_pubkey: Option<String>,
416    /// Trade index the buyer used on the parent order.
417    pub trade_index_buyer: Option<i64>,
418    /// Trade index the seller used on the parent order.
419    pub trade_index_seller: Option<i64>,
420    /// Whether the buyer has initiated a dispute for this order.
421    /// Combined with [`Self::seller_dispute`] to derive
422    /// [`RestoredDisputesInfo::initiator`].
423    pub buyer_dispute: bool,
424    /// Whether the seller has initiated a dispute for this order.
425    /// Combined with [`Self::buyer_dispute`] to derive
426    /// [`RestoredDisputesInfo::initiator`].
427    pub seller_dispute: bool,
428    /// Public key of the solver assigned to the dispute, `None` if no
429    /// solver has taken it.
430    pub solver_pubkey: Option<String>,
431}
432
433/// Minimal per-order information returned to a client on session restore.
434#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
435#[derive(Debug, Deserialize, Serialize, Clone)]
436pub struct RestoredOrdersInfo {
437    /// Id of the order.
438    pub order_id: Uuid,
439    /// Trade index of the order as seen by the requesting user.
440    pub trade_index: i64,
441    /// Current status of the order, serialized as kebab-case.
442    pub status: String,
443}
444
445/// Identifies which party of an order opened a dispute.
446#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
447#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
448#[serde(rename_all = "lowercase")]
449#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
450pub enum DisputeInitiator {
451    /// The buyer opened the dispute.
452    Buyer,
453    /// The seller opened the dispute.
454    Seller,
455}
456
457/// Minimal per-dispute information returned to a client on session restore.
458#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
459#[derive(Debug, Deserialize, Serialize, Clone)]
460pub struct RestoredDisputesInfo {
461    /// Id of the dispute.
462    pub dispute_id: Uuid,
463    /// Id of the order the dispute is attached to.
464    pub order_id: Uuid,
465    /// Trade index of the dispute as seen by the requesting user.
466    pub trade_index: i64,
467    /// Current status of the dispute, serialized as kebab-case.
468    pub status: String,
469    /// Who initiated the dispute: [`DisputeInitiator::Buyer`],
470    /// [`DisputeInitiator::Seller`], or `None` when unknown.
471    pub initiator: Option<DisputeInitiator>,
472    /// Public key of the solver assigned to the dispute, `None` if no
473    /// solver has taken it yet.
474    pub solver_pubkey: Option<String>,
475}
476
477/// Bundle of orders and disputes returned on a session restore.
478///
479/// Carried inside [`Payload::RestoreData`]. The server typically sends this
480/// struct in the response to a [`Action::RestoreSession`] request.
481#[derive(Debug, Deserialize, Serialize, Clone, Default)]
482pub struct RestoreSessionInfo {
483    /// Orders associated with the requesting user.
484    #[serde(rename = "orders")]
485    pub restore_orders: Vec<RestoredOrdersInfo>,
486    /// Disputes associated with the requesting user.
487    #[serde(rename = "disputes")]
488    pub restore_disputes: Vec<RestoredDisputesInfo>,
489}
490
491/// Typed payload attached to a [`MessageKind`].
492///
493/// Each variant corresponds to a set of [`Action`] values that can legally
494/// carry it (see [`MessageKind::verify`]). Serialized in `snake_case` so
495/// that the variant name is the JSON discriminator.
496#[derive(Debug, Deserialize, Serialize, Clone)]
497#[serde(rename_all = "snake_case")]
498pub enum Payload {
499    /// A compact representation of an order used by [`Action::NewOrder`].
500    Order(SmallOrder),
501    /// Lightning payment request plus optional amount override.
502    ///
503    /// Used by [`Action::PayInvoice`], [`Action::AddInvoice`] and
504    /// [`Action::TakeSell`]. The [`SmallOrder`] carries the matching order
505    /// when relevant; the `String` is a BOLT-11 invoice.
506    PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
507    /// Free-form text message used by DMs.
508    TextMessage(String),
509    /// Peer disclosure (trade pubkey and optional reputation).
510    Peer(Peer),
511    /// Rating value the user wants to attach to a completed trade.
512    RatingUser(u8),
513    /// Raw amount in satoshis (for actions that accept an amount override).
514    Amount(Amount),
515    /// Dispute context: the dispute id plus optional
516    /// [`SolverDisputeInfo`] bundle sent to solvers.
517    Dispute(Uuid, Option<SolverDisputeInfo>),
518    /// Reason carried by a [`Action::CantDo`] response.
519    CantDo(Option<CantDoReason>),
520    /// Next trade key and index announced by the maker of a range order
521    /// when it emits [`Action::Release`] or [`Action::FiatSent`].
522    NextTrade(String, u32),
523    /// Retry configuration surfaced by [`Action::PaymentFailed`].
524    PaymentFailed(PaymentFailedInfo),
525    /// Payload returned by the server on a session restore.
526    RestoreData(RestoreSessionInfo),
527    /// Vector of order ids (lightweight listing).
528    Ids(Vec<Uuid>),
529    /// Vector of [`SmallOrder`] values (full listing).
530    Orders(Vec<SmallOrder>),
531}
532
533#[allow(dead_code)]
534impl MessageKind {
535    /// Build a new [`MessageKind`] stamped with the current protocol
536    /// version (`PROTOCOL_VER`).
537    pub fn new(
538        id: Option<Uuid>,
539        request_id: Option<u64>,
540        trade_index: Option<i64>,
541        action: Action,
542        payload: Option<Payload>,
543    ) -> Self {
544        Self {
545            version: PROTOCOL_VER,
546            request_id,
547            trade_index,
548            id,
549            action,
550            payload,
551        }
552    }
553    /// Parse a [`MessageKind`] from its JSON representation.
554    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
555        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
556    }
557    /// Serialize the [`MessageKind`] to a JSON string.
558    pub fn as_json(&self) -> Result<String, ServiceError> {
559        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
560    }
561
562    /// Return a clone of the [`Action`] carried by this message.
563    pub fn get_action(&self) -> Action {
564        self.action.clone()
565    }
566
567    /// Extract the `(next_trade_pubkey, next_trade_index)` pair from a
568    /// [`Payload::NextTrade`] payload.
569    ///
570    /// Returns `Ok(None)` when there is no payload at all and
571    /// [`ServiceError::InvalidPayload`] when the payload is present but of
572    /// a different variant.
573    pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
574        match &self.payload {
575            Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
576            None => Ok(None),
577            _ => Err(ServiceError::InvalidPayload),
578        }
579    }
580
581    /// Extract the rating value from a [`Payload::RatingUser`] payload,
582    /// validating it against
583    /// [`MIN_RATING`]`..=`[`MAX_RATING`].
584    ///
585    /// Returns [`ServiceError::InvalidRating`] when the payload shape is
586    /// wrong and [`ServiceError::InvalidRatingValue`] when the value is out
587    /// of range.
588    pub fn get_rating(&self) -> Result<u8, ServiceError> {
589        if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
590            if !(MIN_RATING..=MAX_RATING).contains(&v) {
591                return Err(ServiceError::InvalidRatingValue);
592            }
593            Ok(v)
594        } else {
595            Err(ServiceError::InvalidRating)
596        }
597    }
598
599    /// Check that the payload, id and trade index are consistent with the
600    /// action carried by this message.
601    ///
602    /// Returns `true` when the combination is well-formed and `false`
603    /// otherwise; Mostro uses this method to reject malformed requests
604    /// before processing them.
605    pub fn verify(&self) -> bool {
606        match &self.action {
607            Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
608            Action::PayInvoice | Action::AddInvoice => {
609                if self.id.is_none() {
610                    return false;
611                }
612                matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
613            }
614            Action::TakeSell
615            | Action::TakeBuy
616            | Action::FiatSent
617            | Action::FiatSentOk
618            | Action::Release
619            | Action::Released
620            | Action::Dispute
621            | Action::AdminCancel
622            | Action::AdminCanceled
623            | Action::AdminSettle
624            | Action::AdminSettled
625            | Action::Rate
626            | Action::RateReceived
627            | Action::AdminTakeDispute
628            | Action::AdminTookDispute
629            | Action::DisputeInitiatedByYou
630            | Action::DisputeInitiatedByPeer
631            | Action::WaitingBuyerInvoice
632            | Action::PurchaseCompleted
633            | Action::HoldInvoicePaymentAccepted
634            | Action::HoldInvoicePaymentSettled
635            | Action::HoldInvoicePaymentCanceled
636            | Action::WaitingSellerToPay
637            | Action::BuyerTookOrder
638            | Action::BuyerInvoiceAccepted
639            | Action::CooperativeCancelInitiatedByYou
640            | Action::CooperativeCancelInitiatedByPeer
641            | Action::CooperativeCancelAccepted
642            | Action::Cancel
643            | Action::InvoiceUpdated
644            | Action::AdminAddSolver
645            | Action::SendDm
646            | Action::TradePubkey
647            | Action::Canceled => {
648                if self.id.is_none() {
649                    return false;
650                }
651                true
652            }
653            Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
654            Action::PaymentFailed => {
655                if self.id.is_none() {
656                    return false;
657                }
658                matches!(&self.payload, Some(Payload::PaymentFailed(_)))
659            }
660            Action::RateUser => {
661                matches!(&self.payload, Some(Payload::RatingUser(_)))
662            }
663            Action::CantDo => {
664                matches!(&self.payload, Some(Payload::CantDo(_)))
665            }
666            Action::Orders => {
667                matches!(
668                    &self.payload,
669                    Some(Payload::Ids(_)) | Some(Payload::Orders(_))
670                )
671            }
672        }
673    }
674
675    /// Return the [`SmallOrder`] carried by a [`Action::NewOrder`] message.
676    ///
677    /// Yields `None` if the action is not `NewOrder` or the payload is of a
678    /// different variant.
679    pub fn get_order(&self) -> Option<&SmallOrder> {
680        if self.action != Action::NewOrder {
681            return None;
682        }
683        match &self.payload {
684            Some(Payload::Order(o)) => Some(o),
685            _ => None,
686        }
687    }
688
689    /// Return the Lightning payment request embedded in a message.
690    ///
691    /// Valid only for [`Action::TakeSell`], [`Action::AddInvoice`] and
692    /// [`Action::NewOrder`]. For `NewOrder`, the invoice is read from the
693    /// [`SmallOrder::buyer_invoice`] field. Returns `None` otherwise.
694    pub fn get_payment_request(&self) -> Option<String> {
695        if self.action != Action::TakeSell
696            && self.action != Action::AddInvoice
697            && self.action != Action::NewOrder
698        {
699            return None;
700        }
701        match &self.payload {
702            Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
703            Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
704            _ => None,
705        }
706    }
707
708    /// Return the amount override embedded in a [`Action::TakeSell`] or
709    /// [`Action::TakeBuy`] message, either from a [`Payload::Amount`] or
710    /// from the third element of a [`Payload::PaymentRequest`].
711    pub fn get_amount(&self) -> Option<Amount> {
712        if self.action != Action::TakeSell && self.action != Action::TakeBuy {
713            return None;
714        }
715        match &self.payload {
716            Some(Payload::PaymentRequest(_, _, amount)) => *amount,
717            Some(Payload::Amount(amount)) => Some(*amount),
718            _ => None,
719        }
720    }
721
722    /// Borrow the optional payload.
723    pub fn get_payload(&self) -> Option<&Payload> {
724        self.payload.as_ref()
725    }
726
727    /// Return `(true, index)` when the message carries a trade index,
728    /// `(false, 0)` otherwise.
729    pub fn has_trade_index(&self) -> (bool, i64) {
730        if let Some(index) = self.trade_index {
731            return (true, index);
732        }
733        (false, 0)
734    }
735
736    /// Return the trade index carried by the message, or `0` when absent.
737    pub fn trade_index(&self) -> i64 {
738        if let Some(index) = self.trade_index {
739            return index;
740        }
741        0
742    }
743}
744
745#[cfg(test)]
746mod test {
747    use crate::message::{Action, Message, MessageKind, Payload, Peer};
748    use crate::user::UserInfo;
749    use nostr_sdk::Keys;
750    use uuid::uuid;
751
752    #[test]
753    fn test_peer_with_reputation() {
754        // Test creating a Peer with reputation information
755        let reputation = UserInfo {
756            rating: 4.5,
757            reviews: 10,
758            operating_days: 30,
759        };
760        let peer = Peer::new(
761            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
762            Some(reputation.clone()),
763        );
764
765        // Assert the fields are set correctly
766        assert_eq!(
767            peer.pubkey,
768            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
769        );
770        assert!(peer.reputation.is_some());
771        let peer_reputation = peer.reputation.clone().unwrap();
772        assert_eq!(peer_reputation.rating, 4.5);
773        assert_eq!(peer_reputation.reviews, 10);
774        assert_eq!(peer_reputation.operating_days, 30);
775
776        // Test JSON serialization and deserialization
777        let json = peer.as_json().unwrap();
778        let deserialized_peer = Peer::from_json(&json).unwrap();
779        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
780        assert!(deserialized_peer.reputation.is_some());
781        let deserialized_reputation = deserialized_peer.reputation.unwrap();
782        assert_eq!(deserialized_reputation.rating, 4.5);
783        assert_eq!(deserialized_reputation.reviews, 10);
784        assert_eq!(deserialized_reputation.operating_days, 30);
785    }
786
787    #[test]
788    fn test_peer_without_reputation() {
789        // Test creating a Peer without reputation information
790        let peer = Peer::new(
791            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
792            None,
793        );
794
795        // Assert the reputation field is None
796        assert_eq!(
797            peer.pubkey,
798            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
799        );
800        assert!(peer.reputation.is_none());
801
802        // Test JSON serialization and deserialization
803        let json = peer.as_json().unwrap();
804        let deserialized_peer = Peer::from_json(&json).unwrap();
805        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
806        assert!(deserialized_peer.reputation.is_none());
807    }
808
809    #[test]
810    fn test_peer_in_message() {
811        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
812
813        // Test with reputation
814        let reputation = UserInfo {
815            rating: 4.5,
816            reviews: 10,
817            operating_days: 30,
818        };
819        let peer_with_reputation = Peer::new(
820            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
821            Some(reputation),
822        );
823        let payload_with_reputation = Payload::Peer(peer_with_reputation);
824        let message_with_reputation = Message::Order(MessageKind::new(
825            Some(uuid),
826            Some(1),
827            Some(2),
828            Action::FiatSentOk,
829            Some(payload_with_reputation),
830        ));
831
832        // Verify message with reputation
833        assert!(message_with_reputation.verify());
834        let message_json = message_with_reputation.as_json().unwrap();
835        let deserialized_message = Message::from_json(&message_json).unwrap();
836        assert!(deserialized_message.verify());
837
838        // Test without reputation
839        let peer_without_reputation = Peer::new(
840            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
841            None,
842        );
843        let payload_without_reputation = Payload::Peer(peer_without_reputation);
844        let message_without_reputation = Message::Order(MessageKind::new(
845            Some(uuid),
846            Some(1),
847            Some(2),
848            Action::FiatSentOk,
849            Some(payload_without_reputation),
850        ));
851
852        // Verify message without reputation
853        assert!(message_without_reputation.verify());
854        let message_json = message_without_reputation.as_json().unwrap();
855        let deserialized_message = Message::from_json(&message_json).unwrap();
856        assert!(deserialized_message.verify());
857    }
858
859    #[test]
860    fn test_payment_failed_payload() {
861        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
862
863        // Test PaymentFailedInfo serialization and deserialization
864        let payment_failed_info = crate::message::PaymentFailedInfo {
865            payment_attempts: 3,
866            payment_retries_interval: 60,
867        };
868
869        let payload = Payload::PaymentFailed(payment_failed_info);
870        let message = Message::Order(MessageKind::new(
871            Some(uuid),
872            Some(1),
873            Some(2),
874            Action::PaymentFailed,
875            Some(payload),
876        ));
877
878        // Verify message validation
879        assert!(message.verify());
880
881        // Test JSON serialization
882        let message_json = message.as_json().unwrap();
883
884        // Test deserialization
885        let deserialized_message = Message::from_json(&message_json).unwrap();
886        assert!(deserialized_message.verify());
887
888        // Verify the payload contains correct values
889        if let Message::Order(kind) = deserialized_message {
890            if let Some(Payload::PaymentFailed(info)) = kind.payload {
891                assert_eq!(info.payment_attempts, 3);
892                assert_eq!(info.payment_retries_interval, 60);
893            } else {
894                panic!("Expected PaymentFailed payload");
895            }
896        } else {
897            panic!("Expected Order message");
898        }
899    }
900
901    #[test]
902    fn test_message_payload_signature() {
903        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
904        let peer = Peer::new(
905            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
906            None, // Add None for the reputation parameter
907        );
908        let payload = Payload::Peer(peer);
909        let test_message = Message::Order(MessageKind::new(
910            Some(uuid),
911            Some(1),
912            Some(2),
913            Action::FiatSentOk,
914            Some(payload),
915        ));
916        assert!(test_message.verify());
917        let test_message_json = test_message.as_json().unwrap();
918        // Message should be signed with the trade keys
919        let trade_keys =
920            Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
921                .unwrap();
922        let sig = Message::sign(test_message_json.clone(), &trade_keys);
923
924        assert!(Message::verify_signature(
925            test_message_json,
926            trade_keys.public_key(),
927            sig
928        ));
929    }
930
931    #[test]
932    fn test_restore_session_message() {
933        // Test RestoreSession request (payload = None)
934        let restore_request_message = Message::Restore(MessageKind::new(
935            None,
936            None,
937            None,
938            Action::RestoreSession,
939            None,
940        ));
941
942        // Verify message validation
943        assert!(restore_request_message.verify());
944        assert_eq!(
945            restore_request_message.inner_action(),
946            Some(Action::RestoreSession)
947        );
948
949        // Test JSON serialization and deserialization for RestoreRequest
950        let message_json = restore_request_message.as_json().unwrap();
951        let deserialized_message = Message::from_json(&message_json).unwrap();
952        assert!(deserialized_message.verify());
953        assert_eq!(
954            deserialized_message.inner_action(),
955            Some(Action::RestoreSession)
956        );
957
958        // Test RestoreSession with RestoreData payload
959        let restored_orders = vec![
960            crate::message::RestoredOrdersInfo {
961                order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
962                trade_index: 1,
963                status: "active".to_string(),
964            },
965            crate::message::RestoredOrdersInfo {
966                order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
967                trade_index: 2,
968                status: "success".to_string(),
969            },
970        ];
971
972        let restored_disputes = vec![
973            crate::message::RestoredDisputesInfo {
974                dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
975                order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
976                trade_index: 1,
977                status: "initiated".to_string(),
978                initiator: Some(crate::message::DisputeInitiator::Buyer),
979                solver_pubkey: None,
980            },
981            crate::message::RestoredDisputesInfo {
982                dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
983                order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
984                trade_index: 2,
985                status: "in-progress".to_string(),
986                initiator: None,
987                solver_pubkey: Some(
988                    "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
989                ),
990            },
991            crate::message::RestoredDisputesInfo {
992                dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
993                order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
994                trade_index: 3,
995                status: "initiated".to_string(),
996                initiator: Some(crate::message::DisputeInitiator::Seller),
997                solver_pubkey: None,
998            },
999        ];
1000
1001        let restore_session_info = crate::message::RestoreSessionInfo {
1002            restore_orders: restored_orders.clone(),
1003            restore_disputes: restored_disputes.clone(),
1004        };
1005
1006        let restore_data_payload = Payload::RestoreData(restore_session_info);
1007        let restore_data_message = Message::Restore(MessageKind::new(
1008            None,
1009            None,
1010            None,
1011            Action::RestoreSession,
1012            Some(restore_data_payload),
1013        ));
1014
1015        // With new logic, any payload for RestoreSession is invalid (must be None)
1016        assert!(!restore_data_message.verify());
1017
1018        // Verify serialization/deserialization of RestoreData payload with all initiator cases
1019        let message_json = restore_data_message.as_json().unwrap();
1020        let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1021
1022        if let Message::Restore(kind) = deserialized_restore_message {
1023            if let Some(Payload::RestoreData(session_info)) = kind.payload {
1024                assert_eq!(session_info.restore_disputes.len(), 3);
1025                assert_eq!(
1026                    session_info.restore_disputes[0].initiator,
1027                    Some(crate::message::DisputeInitiator::Buyer)
1028                );
1029                assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1030                assert_eq!(session_info.restore_disputes[1].initiator, None);
1031                assert_eq!(
1032                    session_info.restore_disputes[1].solver_pubkey,
1033                    Some(
1034                        "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1035                            .to_string()
1036                    )
1037                );
1038                assert_eq!(
1039                    session_info.restore_disputes[2].initiator,
1040                    Some(crate::message::DisputeInitiator::Seller)
1041                );
1042                assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1043            } else {
1044                panic!("Expected RestoreData payload");
1045            }
1046        } else {
1047            panic!("Expected Restore message");
1048        }
1049    }
1050
1051    #[test]
1052    fn test_restore_session_message_validation() {
1053        // Test that RestoreSession action accepts only payload=None or RestoreData
1054        let restore_request_message = Message::Restore(MessageKind::new(
1055            None,
1056            None,
1057            None,
1058            Action::RestoreSession,
1059            None, // Missing payload
1060        ));
1061
1062        // Verify restore request message
1063        assert!(restore_request_message.verify());
1064
1065        // Test with wrong payload type
1066        let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1067        let wrong_message = Message::Restore(MessageKind::new(
1068            None,
1069            None,
1070            None,
1071            Action::RestoreSession,
1072            Some(wrong_payload),
1073        ));
1074
1075        // Should fail validation because RestoreSession only accepts None
1076        assert!(!wrong_message.verify());
1077
1078        // With new logic, presence of id/request_id/trade_index is allowed
1079        let with_id = Message::Restore(MessageKind::new(
1080            Some(uuid!("00000000-0000-0000-0000-000000000001")),
1081            None,
1082            None,
1083            Action::RestoreSession,
1084            None,
1085        ));
1086        assert!(with_id.verify());
1087
1088        let with_request_id = Message::Restore(MessageKind::new(
1089            None,
1090            Some(42),
1091            None,
1092            Action::RestoreSession,
1093            None,
1094        ));
1095        assert!(with_request_id.verify());
1096
1097        let with_trade_index = Message::Restore(MessageKind::new(
1098            None,
1099            None,
1100            Some(7),
1101            Action::RestoreSession,
1102            None,
1103        ));
1104        assert!(with_trade_index.verify());
1105    }
1106
1107    #[test]
1108    fn test_restore_session_message_constructor() {
1109        // Test the new_restore constructor
1110        let restore_request_message = Message::new_restore(None);
1111
1112        assert!(matches!(restore_request_message, Message::Restore(_)));
1113        assert!(restore_request_message.verify());
1114        assert_eq!(
1115            restore_request_message.inner_action(),
1116            Some(Action::RestoreSession)
1117        );
1118
1119        // Test with RestoreData payload should be invalid now
1120        let restore_session_info = crate::message::RestoreSessionInfo {
1121            restore_orders: vec![],
1122            restore_disputes: vec![],
1123        };
1124        let restore_data_message =
1125            Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1126
1127        assert!(matches!(restore_data_message, Message::Restore(_)));
1128        assert!(!restore_data_message.verify());
1129    }
1130
1131    #[test]
1132    fn test_last_trade_index_valid_message() {
1133        let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1134        let msg = Message::Restore(kind);
1135
1136        assert!(msg.verify());
1137
1138        // roundtrip
1139        let json = msg.as_json().unwrap();
1140        let decoded = Message::from_json(&json).unwrap();
1141        assert!(decoded.verify());
1142
1143        // ensure the trade index is propagated
1144        let inner = decoded.get_inner_message_kind();
1145        assert_eq!(inner.trade_index(), 7);
1146        assert_eq!(inner.has_trade_index(), (true, 7));
1147    }
1148
1149    #[test]
1150    fn test_last_trade_index_without_id_is_valid() {
1151        // With new logic, id is not required; only payload must be None
1152        let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1153        let msg = Message::Restore(kind);
1154        assert!(msg.verify());
1155    }
1156
1157    #[test]
1158    fn test_last_trade_index_with_payload_fails_validation() {
1159        // LastTradeIndex does not accept payload
1160        let kind = MessageKind::new(
1161            None,
1162            None,
1163            Some(3),
1164            Action::LastTradeIndex,
1165            Some(Payload::TextMessage("ignored".to_string())),
1166        );
1167        let msg = Message::Restore(kind);
1168        assert!(!msg.verify());
1169    }
1170
1171    #[test]
1172    fn test_restored_dispute_helper_serialization_roundtrip() {
1173        use crate::message::RestoredDisputeHelper;
1174
1175        let helper = RestoredDisputeHelper {
1176            dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1177            order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1178            dispute_status: "initiated".to_string(),
1179            master_buyer_pubkey: Some("npub1buyerkey".to_string()),
1180            master_seller_pubkey: Some("npub1sellerkey".to_string()),
1181            trade_index_buyer: Some(1),
1182            trade_index_seller: Some(2),
1183            buyer_dispute: true,
1184            seller_dispute: false,
1185            solver_pubkey: None,
1186        };
1187
1188        let json = serde_json::to_string(&helper).unwrap();
1189        let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
1190
1191        assert_eq!(deserialized.dispute_id, helper.dispute_id);
1192        assert_eq!(deserialized.order_id, helper.order_id);
1193        assert_eq!(deserialized.dispute_status, helper.dispute_status);
1194        assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
1195        assert_eq!(
1196            deserialized.master_seller_pubkey,
1197            helper.master_seller_pubkey
1198        );
1199        assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
1200        assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
1201        assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
1202        assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
1203        assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
1204
1205        let helper_seller_dispute = RestoredDisputeHelper {
1206            dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1207            order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1208            dispute_status: "in-progress".to_string(),
1209            master_buyer_pubkey: None,
1210            master_seller_pubkey: None,
1211            trade_index_buyer: None,
1212            trade_index_seller: None,
1213            buyer_dispute: false,
1214            seller_dispute: true,
1215            solver_pubkey: Some(
1216                "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1217            ),
1218        };
1219
1220        let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
1221        let deserialized_seller: RestoredDisputeHelper =
1222            serde_json::from_str(&json_seller).unwrap();
1223
1224        assert_eq!(
1225            deserialized_seller.dispute_id,
1226            helper_seller_dispute.dispute_id
1227        );
1228        assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
1229        assert_eq!(
1230            deserialized_seller.dispute_status,
1231            helper_seller_dispute.dispute_status
1232        );
1233        assert_eq!(deserialized_seller.master_buyer_pubkey, None);
1234        assert_eq!(deserialized_seller.master_seller_pubkey, None);
1235        assert_eq!(deserialized_seller.trade_index_buyer, None);
1236        assert_eq!(deserialized_seller.trade_index_seller, None);
1237        assert!(!deserialized_seller.buyer_dispute);
1238        assert!(deserialized_seller.seller_dispute);
1239        assert_eq!(
1240            deserialized_seller.solver_pubkey,
1241            helper_seller_dispute.solver_pubkey
1242        );
1243    }
1244}