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    /// Mostro delivers the bolt11 hold invoice that the taker must pay as
78    /// their anti-abuse bond. Same payload shape and direction as
79    /// [`Action::PayInvoice`] (Mostro → user); only the discriminator
80    /// differs so clients can tell the bond invoice apart from the trade
81    /// hold invoice that follows.
82    /// Payload: [`Payload::PaymentRequest`].
83    PayBondInvoice,
84    /// Buyer notifies Mostro that fiat was sent.
85    FiatSent,
86    /// Mostro acknowledges the fiat-sent notification to the seller.
87    FiatSentOk,
88    /// Seller releases the hold invoice funds.
89    Release,
90    /// Mostro confirms that the funds have been released.
91    Released,
92    /// Cancel an order.
93    Cancel,
94    /// Mostro confirms that the order was canceled.
95    Canceled,
96    /// Local side started a cooperative cancel.
97    CooperativeCancelInitiatedByYou,
98    /// Remote side started a cooperative cancel.
99    CooperativeCancelInitiatedByPeer,
100    /// Local side opened a dispute.
101    DisputeInitiatedByYou,
102    /// Remote side opened a dispute.
103    DisputeInitiatedByPeer,
104    /// Both sides agreed on the cooperative cancel.
105    CooperativeCancelAccepted,
106    /// Mostro accepted the buyer's payout invoice.
107    BuyerInvoiceAccepted,
108    /// Mostro accepted the winning counterparty's bond-payout invoice.
109    /// Bond dual of [`Action::BuyerInvoiceAccepted`] (Mostro → winner):
110    /// the payout bolt11 was received and the payment is now pending, so
111    /// the client can stop prompting the user for an invoice.
112    /// Payload: [`Payload::Order`] (amount = counterparty share).
113    BondInvoiceAccepted,
114    /// Trade completed successfully.
115    PurchaseCompleted,
116    /// Mostro paid out a slashed bond's counterparty share successfully.
117    /// Bond dual of [`Action::PurchaseCompleted`] (Mostro → winner): the
118    /// `send_payment` to the winner's bolt11 succeeded and the bond is
119    /// now settled, so the client can close the claim.
120    /// Payload: [`Payload::Order`] (amount = counterparty share).
121    BondPayoutCompleted,
122    /// Mostro saw the hold-invoice payment accepted by the node.
123    HoldInvoicePaymentAccepted,
124    /// Mostro saw the hold-invoice payment settled.
125    HoldInvoicePaymentSettled,
126    /// Mostro saw the hold-invoice payment canceled.
127    HoldInvoicePaymentCanceled,
128    /// Informational: waiting for the seller to pay the hold invoice.
129    WaitingSellerToPay,
130    /// Informational: waiting for the buyer's payout invoice.
131    WaitingBuyerInvoice,
132    /// Buyer sends/updates its payout invoice.
133    /// Payload: [`Payload::PaymentRequest`].
134    AddInvoice,
135    /// Taker sends a Lightning invoice that Mostro must pay out as the
136    /// taker's anti-abuse bond. Same payload shape and direction as
137    /// [`Action::AddInvoice`] (user → Mostro); only the discriminator
138    /// differs so Mostro can tell a bond-payout invoice apart from a
139    /// buyer's trade payout invoice.
140    /// Payload: [`Payload::PaymentRequest`].
141    AddBondInvoice,
142    /// Informational: a buyer has taken a sell order.
143    BuyerTookOrder,
144    /// Server-initiated rating request.
145    Rate,
146    /// Client-initiated rate. Payload: [`Payload::RatingUser`].
147    RateUser,
148    /// Acknowledgement of a received rating.
149    RateReceived,
150    /// Mostro returns a structured refusal. Payload: [`Payload::CantDo`].
151    CantDo,
152    /// Client-initiated dispute.
153    Dispute,
154    /// Admin cancels a trade.
155    AdminCancel,
156    /// Admin cancel acknowledged.
157    AdminCanceled,
158    /// Admin settles the hold invoice.
159    AdminSettle,
160    /// Admin settle acknowledged.
161    AdminSettled,
162    /// Admin registers a new dispute solver.
163    AdminAddSolver,
164    /// Solver takes a dispute.
165    AdminTakeDispute,
166    /// Solver took the dispute acknowledged.
167    AdminTookDispute,
168    /// Notification that a Lightning payment failed.
169    /// Payload: [`Payload::PaymentFailed`].
170    PaymentFailed,
171    /// Invoice associated with the order was updated.
172    InvoiceUpdated,
173    /// Direct message between users. Payload: [`Payload::TextMessage`].
174    SendDm,
175    /// Disclosure of a counterpart's trade pubkey. Payload: [`Payload::Peer`].
176    TradePubkey,
177    /// Client asks Mostro to restore its session state. Payload must be `None`.
178    RestoreSession,
179    /// Client asks Mostro for its last known trade index. Payload must be
180    /// `None`.
181    LastTradeIndex,
182    /// Listing of orders in response to a query.
183    /// Payload: [`Payload::Ids`] or [`Payload::Orders`].
184    Orders,
185}
186
187impl fmt::Display for Action {
188    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
189        write!(f, "{self:?}")
190    }
191}
192
193/// Top-level Mostro message exchanged between users and Mostro.
194///
195/// `Message` is a tagged union: every variant carries the shared
196/// [`MessageKind`] body, while the variant itself tells the receiver which
197/// channel the message belongs to (orders, disputes, DMs, rating, can't-do,
198/// session restore). Serializes as `kebab-case` JSON.
199#[derive(Debug, Clone, Deserialize, Serialize)]
200#[serde(rename_all = "kebab-case")]
201pub enum Message {
202    /// Order-channel message.
203    Order(MessageKind),
204    /// Dispute-channel message.
205    Dispute(MessageKind),
206    /// "Can't do" response returned by the Mostro node.
207    CantDo(MessageKind),
208    /// Rating message (server-initiated rate request or client rate).
209    Rate(MessageKind),
210    /// Direct message between users.
211    Dm(MessageKind),
212    /// Session restore request/response.
213    Restore(MessageKind),
214}
215
216impl Message {
217    /// Build a new `Message::Order` wrapping a freshly constructed
218    /// [`MessageKind`].
219    pub fn new_order(
220        id: Option<Uuid>,
221        request_id: Option<u64>,
222        trade_index: Option<i64>,
223        action: Action,
224        payload: Option<Payload>,
225    ) -> Self {
226        let kind = MessageKind::new(id, request_id, trade_index, action, payload);
227        Self::Order(kind)
228    }
229
230    /// Build a new `Message::Dispute` wrapping a freshly constructed
231    /// [`MessageKind`].
232    pub fn new_dispute(
233        id: Option<Uuid>,
234        request_id: Option<u64>,
235        trade_index: Option<i64>,
236        action: Action,
237        payload: Option<Payload>,
238    ) -> Self {
239        let kind = MessageKind::new(id, request_id, trade_index, action, payload);
240
241        Self::Dispute(kind)
242    }
243
244    /// Build a new `Message::Restore` with [`Action::RestoreSession`].
245    ///
246    /// According to [`MessageKind::verify`], the payload for a restore
247    /// request must be `None`. Any other payload yields an invalid message.
248    pub fn new_restore(payload: Option<Payload>) -> Self {
249        let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
250        Self::Restore(kind)
251    }
252
253    /// Build a new `Message::CantDo` message (a structured refusal sent by
254    /// Mostro when a request cannot be fulfilled).
255    pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
256        let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
257
258        Self::CantDo(kind)
259    }
260
261    /// Build a new `Message::Dm` carrying a direct message between users.
262    pub fn new_dm(
263        id: Option<Uuid>,
264        request_id: Option<u64>,
265        action: Action,
266        payload: Option<Payload>,
267    ) -> Self {
268        let kind = MessageKind::new(id, request_id, None, action, payload);
269
270        Self::Dm(kind)
271    }
272
273    /// Parse a [`Message`] from its JSON representation.
274    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
275        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
276    }
277
278    /// Serialize the message to a JSON string.
279    pub fn as_json(&self) -> Result<String, ServiceError> {
280        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
281    }
282
283    /// Borrow the inner [`MessageKind`] regardless of the variant.
284    pub fn get_inner_message_kind(&self) -> &MessageKind {
285        match self {
286            Message::Dispute(k)
287            | Message::Order(k)
288            | Message::CantDo(k)
289            | Message::Rate(k)
290            | Message::Dm(k)
291            | Message::Restore(k) => k,
292        }
293    }
294
295    /// Return the [`Action`] of the inner [`MessageKind`].
296    ///
297    /// Always returns `Some` for the current variant set; the `Option` is
298    /// kept for API stability.
299    pub fn inner_action(&self) -> Option<Action> {
300        match self {
301            Message::Dispute(a)
302            | Message::Order(a)
303            | Message::CantDo(a)
304            | Message::Rate(a)
305            | Message::Dm(a)
306            | Message::Restore(a) => Some(a.get_action()),
307        }
308    }
309
310    /// Validate that the inner [`MessageKind`] is consistent with its
311    /// [`Action`]. Delegates to [`MessageKind::verify`].
312    pub fn verify(&self) -> bool {
313        match self {
314            Message::Order(m)
315            | Message::Dispute(m)
316            | Message::CantDo(m)
317            | Message::Rate(m)
318            | Message::Dm(m)
319            | Message::Restore(m) => m.verify(),
320        }
321    }
322
323    /// Produce a Schnorr signature over the SHA-256 digest of `message`
324    /// using `keys`.
325    ///
326    /// This is the signature embedded in the rumor tuple when
327    /// [`crate::nip59::wrap_message`] is called with
328    /// [`WrapOptions::signed`](crate::nip59::WrapOptions::signed) set to
329    /// `true`. It binds a message to the sender's trade keys without
330    /// relying on the outer Nostr event signature.
331    pub fn sign(message: String, keys: &Keys) -> Signature {
332        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
333        let hash = hash.to_byte_array();
334        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
335
336        keys.sign_schnorr(&message)
337    }
338
339    /// Verify a signature previously produced by [`Message::sign`].
340    ///
341    /// Returns `true` when `sig` is a valid Schnorr signature of the
342    /// SHA-256 digest of `message` under `pubkey`, `false` otherwise
343    /// (including when `pubkey` has no x-only representation).
344    pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
345        // Create payload hash
346        let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
347        let hash = hash.to_byte_array();
348        let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
349
350        // Create a verification-only context for better performance
351        let secp = Secp256k1::verification_only();
352        // Verify signature
353        if let Ok(xonlykey) = pubkey.xonly() {
354            xonlykey.verify(&secp, &message, &sig).is_ok()
355        } else {
356            false
357        }
358    }
359}
360
361/// Body shared by every [`Message`] variant.
362///
363/// All Mostro protocol messages share this envelope: a protocol version,
364/// an optional client-chosen request id for correlation, a trade index used
365/// to enforce strictly increasing sequences per user, an optional
366/// order/dispute id, an [`Action`] and an optional [`Payload`].
367#[derive(Debug, Clone, Deserialize, Serialize)]
368pub struct MessageKind {
369    /// Mostro protocol version. Set to
370    /// `PROTOCOL_VER` by [`MessageKind::new`].
371    pub version: u8,
372    /// Client-chosen correlation id, echoed back on responses so the client
373    /// can match them to in-flight requests.
374    pub request_id: Option<u64>,
375    /// Trade index attached to this message. Must be strictly greater than
376    /// the last trade index Mostro has seen for the sender.
377    pub trade_index: Option<i64>,
378    /// Optional target identifier (usually the id of an [`crate::order::Order`]
379    /// or [`crate::dispute::Dispute`]).
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub id: Option<Uuid>,
382    /// Verb of the message.
383    pub action: Action,
384    /// Payload attached to the action. The allowed shape for a given action
385    /// is enforced by [`MessageKind::verify`].
386    pub payload: Option<Payload>,
387}
388
389/// Alias for a signed integer amount in satoshis.
390type Amount = i64;
391
392/// Retry configuration for a failed Lightning payment.
393///
394/// Sent inside a [`Payload::PaymentFailed`] so the client knows how many
395/// retries to expect and how long to wait between them.
396#[derive(Debug, Deserialize, Serialize, Clone)]
397pub struct PaymentFailedInfo {
398    /// Maximum number of payment attempts Mostro will perform.
399    pub payment_attempts: u32,
400    /// Delay in seconds between two retry attempts.
401    pub payment_retries_interval: u32,
402}
403
404/// Row-mapper used by `mostrod` when fetching metadata for session restore.
405///
406/// Not intended as a general-purpose order representation — field names are
407/// chosen to match the SQL `SELECT` aliases used by the server query.
408#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
409#[derive(Debug, Deserialize, Serialize, Clone)]
410pub struct RestoredOrderHelper {
411    /// Order id.
412    pub id: Uuid,
413    /// Order status, serialized as kebab-case.
414    pub status: String,
415    /// Master identity pubkey of the buyer, if any.
416    pub master_buyer_pubkey: Option<String>,
417    /// Master identity pubkey of the seller, if any.
418    pub master_seller_pubkey: Option<String>,
419    /// Trade index the buyer used on this order.
420    pub trade_index_buyer: Option<i64>,
421    /// Trade index the seller used on this order.
422    pub trade_index_seller: Option<i64>,
423}
424
425/// Row-mapper used by `mostrod` when fetching disputes for session restore.
426///
427/// Field names are chosen to match the SQL `SELECT` aliases in the restore
428/// query (in particular `status` is aliased as `dispute_status`).
429#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
430#[derive(Debug, Deserialize, Serialize, Clone)]
431pub struct RestoredDisputeHelper {
432    /// Dispute id.
433    pub dispute_id: Uuid,
434    /// Order id the dispute is attached to.
435    pub order_id: Uuid,
436    /// Dispute status, serialized as kebab-case.
437    pub dispute_status: String,
438    /// Master identity pubkey of the buyer, if any.
439    pub master_buyer_pubkey: Option<String>,
440    /// Master identity pubkey of the seller, if any.
441    pub master_seller_pubkey: Option<String>,
442    /// Trade index the buyer used on the parent order.
443    pub trade_index_buyer: Option<i64>,
444    /// Trade index the seller used on the parent order.
445    pub trade_index_seller: Option<i64>,
446    /// Whether the buyer has initiated a dispute for this order.
447    /// Combined with [`Self::seller_dispute`] to derive
448    /// [`RestoredDisputesInfo::initiator`].
449    pub buyer_dispute: bool,
450    /// Whether the seller has initiated a dispute for this order.
451    /// Combined with [`Self::buyer_dispute`] to derive
452    /// [`RestoredDisputesInfo::initiator`].
453    pub seller_dispute: bool,
454    /// Public key of the solver assigned to the dispute, `None` if no
455    /// solver has taken it.
456    pub solver_pubkey: Option<String>,
457}
458
459/// Minimal per-order information returned to a client on session restore.
460#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
461#[derive(Debug, Deserialize, Serialize, Clone)]
462pub struct RestoredOrdersInfo {
463    /// Id of the order.
464    pub order_id: Uuid,
465    /// Trade index of the order as seen by the requesting user.
466    pub trade_index: i64,
467    /// Current status of the order, serialized as kebab-case.
468    pub status: String,
469}
470
471/// Identifies which party of an order opened a dispute.
472#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
473#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
474#[serde(rename_all = "lowercase")]
475#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
476pub enum DisputeInitiator {
477    /// The buyer opened the dispute.
478    Buyer,
479    /// The seller opened the dispute.
480    Seller,
481}
482
483/// Minimal per-dispute information returned to a client on session restore.
484#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
485#[derive(Debug, Deserialize, Serialize, Clone)]
486pub struct RestoredDisputesInfo {
487    /// Id of the dispute.
488    pub dispute_id: Uuid,
489    /// Id of the order the dispute is attached to.
490    pub order_id: Uuid,
491    /// Trade index of the dispute as seen by the requesting user.
492    pub trade_index: i64,
493    /// Current status of the dispute, serialized as kebab-case.
494    pub status: String,
495    /// Who initiated the dispute: [`DisputeInitiator::Buyer`],
496    /// [`DisputeInitiator::Seller`], or `None` when unknown.
497    pub initiator: Option<DisputeInitiator>,
498    /// Public key of the solver assigned to the dispute, `None` if no
499    /// solver has taken it yet.
500    pub solver_pubkey: Option<String>,
501}
502
503/// Bundle of orders and disputes returned on a session restore.
504///
505/// Carried inside [`Payload::RestoreData`]. The server typically sends this
506/// struct in the response to a [`Action::RestoreSession`] request.
507#[derive(Debug, Deserialize, Serialize, Clone, Default)]
508pub struct RestoreSessionInfo {
509    /// Orders associated with the requesting user.
510    #[serde(rename = "orders")]
511    pub restore_orders: Vec<RestoredOrdersInfo>,
512    /// Disputes associated with the requesting user.
513    #[serde(rename = "disputes")]
514    pub restore_disputes: Vec<RestoredDisputesInfo>,
515}
516
517/// Bond resolution carried by [`Action::AdminSettle`] /
518/// [`Action::AdminCancel`].
519///
520/// Lets the solver express slash decisions independently from the trade
521/// outcome (settle vs cancel). Absent payload (`null`) ⇒ neither bond is
522/// slashed (release-by-default, coherent with the "when in doubt, release"
523/// invariant).
524#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
525pub struct BondResolution {
526    /// Slash the seller's bond (if posted).
527    pub slash_seller: bool,
528    /// Slash the buyer's bond (if posted).
529    pub slash_buyer: bool,
530}
531
532/// Outbound side of the bond payout invoice request carried by
533/// [`Action::AddBondInvoice`] (Mostro → winning counterparty).
534///
535/// Asks the recipient for a bolt11 sized at `order.amount` (= the
536/// counterparty share of a slashed bond) and ships the slash anchor
537/// `slashed_at` so the client can compute the forfeit deadline as
538/// `slashed_at + bond_payout_claim_window_days * 86_400` — accurate even
539/// when the message lands days late because the recipient or their relay
540/// was offline.
541///
542/// The reply (counterparty → Mostro) reuses [`Payload::PaymentRequest`]
543/// with the actual bolt11 in the second tuple slot, so the two
544/// directions of the same action are wire-distinguishable by payload
545/// shape rather than by message ordering.
546#[derive(Debug, Deserialize, Serialize, Clone)]
547pub struct BondPayoutRequest {
548    /// Order context (id, kind, `amount` = counterparty share in sats,
549    /// fiat metadata, etc.). Same [`SmallOrder`] shape the client
550    /// already renders for other order-bearing actions.
551    pub order: SmallOrder,
552    /// Unix timestamp (seconds, UTC) at which Mostro recorded the slash
553    /// decision. Frozen at the `Locked → PendingPayout` CAS and shipped
554    /// verbatim on every cadence retry of this request — clients can
555    /// rely on it as a fixed anchor.
556    pub slashed_at: i64,
557}
558
559/// Typed payload attached to a [`MessageKind`].
560///
561/// Each variant corresponds to a set of [`Action`] values that can legally
562/// carry it (see [`MessageKind::verify`]). Serialized in `snake_case` so
563/// that the variant name is the JSON discriminator.
564#[derive(Debug, Deserialize, Serialize, Clone)]
565#[serde(rename_all = "snake_case")]
566pub enum Payload {
567    /// A compact representation of an order used by [`Action::NewOrder`].
568    Order(SmallOrder),
569    /// Lightning payment request plus optional amount override.
570    ///
571    /// Used by [`Action::PayInvoice`], [`Action::PayBondInvoice`],
572    /// [`Action::AddInvoice`], [`Action::AddBondInvoice`] and
573    /// [`Action::TakeSell`]. The [`SmallOrder`]
574    /// carries the matching order when relevant; the `String` is a BOLT-11
575    /// invoice.
576    PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
577    /// Free-form text message used by DMs.
578    TextMessage(String),
579    /// Peer disclosure (trade pubkey and optional reputation).
580    Peer(Peer),
581    /// Rating value the user wants to attach to a completed trade.
582    RatingUser(u8),
583    /// Raw amount in satoshis (for actions that accept an amount override).
584    Amount(Amount),
585    /// Dispute context: the dispute id plus optional
586    /// [`SolverDisputeInfo`] bundle sent to solvers.
587    Dispute(Uuid, Option<SolverDisputeInfo>),
588    /// Reason carried by a [`Action::CantDo`] response.
589    CantDo(Option<CantDoReason>),
590    /// Next trade key and index announced by the maker of a range order
591    /// when it emits [`Action::Release`] or [`Action::FiatSent`].
592    NextTrade(String, u32),
593    /// Retry configuration surfaced by [`Action::PaymentFailed`].
594    PaymentFailed(PaymentFailedInfo),
595    /// Payload returned by the server on a session restore.
596    RestoreData(RestoreSessionInfo),
597    /// Vector of order ids (lightweight listing).
598    Ids(Vec<Uuid>),
599    /// Vector of [`SmallOrder`] values (full listing).
600    Orders(Vec<SmallOrder>),
601    /// Slash decisions carried by [`Action::AdminSettle`] /
602    /// [`Action::AdminCancel`]. See [`BondResolution`].
603    BondResolution(BondResolution),
604    /// Outbound bond payout invoice request carried by
605    /// [`Action::AddBondInvoice`] (Mostro → counterparty). The reply
606    /// direction (counterparty → Mostro) keeps using
607    /// [`Payload::PaymentRequest`] with the actual bolt11. See
608    /// [`BondPayoutRequest`].
609    BondPayoutRequest(BondPayoutRequest),
610}
611
612#[allow(dead_code)]
613impl MessageKind {
614    /// Build a new [`MessageKind`] stamped with the current protocol
615    /// version (`PROTOCOL_VER`).
616    pub fn new(
617        id: Option<Uuid>,
618        request_id: Option<u64>,
619        trade_index: Option<i64>,
620        action: Action,
621        payload: Option<Payload>,
622    ) -> Self {
623        Self {
624            version: PROTOCOL_VER,
625            request_id,
626            trade_index,
627            id,
628            action,
629            payload,
630        }
631    }
632    /// Parse a [`MessageKind`] from its JSON representation.
633    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
634        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
635    }
636    /// Serialize the [`MessageKind`] to a JSON string.
637    pub fn as_json(&self) -> Result<String, ServiceError> {
638        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
639    }
640
641    /// Return a clone of the [`Action`] carried by this message.
642    pub fn get_action(&self) -> Action {
643        self.action.clone()
644    }
645
646    /// Extract the `(next_trade_pubkey, next_trade_index)` pair from a
647    /// [`Payload::NextTrade`] payload.
648    ///
649    /// Returns `Ok(None)` when there is no payload at all and
650    /// [`ServiceError::InvalidPayload`] when the payload is present but of
651    /// a different variant.
652    pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
653        match &self.payload {
654            Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
655            None => Ok(None),
656            _ => Err(ServiceError::InvalidPayload),
657        }
658    }
659
660    /// Extract the rating value from a [`Payload::RatingUser`] payload,
661    /// validating it against
662    /// [`MIN_RATING`]`..=`[`MAX_RATING`].
663    ///
664    /// Returns [`ServiceError::InvalidRating`] when the payload shape is
665    /// wrong and [`ServiceError::InvalidRatingValue`] when the value is out
666    /// of range.
667    pub fn get_rating(&self) -> Result<u8, ServiceError> {
668        if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
669            if !(MIN_RATING..=MAX_RATING).contains(&v) {
670                return Err(ServiceError::InvalidRatingValue);
671            }
672            Ok(v)
673        } else {
674            Err(ServiceError::InvalidRating)
675        }
676    }
677
678    /// Check that the payload, id and trade index are consistent with the
679    /// action carried by this message.
680    ///
681    /// Returns `true` when the combination is well-formed and `false`
682    /// otherwise; Mostro uses this method to reject malformed requests
683    /// before processing them.
684    pub fn verify(&self) -> bool {
685        match &self.action {
686            Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
687            Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
688                if self.id.is_none() {
689                    return false;
690                }
691                matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
692            }
693            Action::AddBondInvoice => {
694                if self.id.is_none() {
695                    return false;
696                }
697                // Two valid shapes:
698                //   - `BondPayoutRequest` for the outbound direction
699                //     (Mostro → counterparty, "send me a bolt11").
700                //   - `PaymentRequest` for the inbound reply
701                //     (counterparty → Mostro, "here is the bolt11").
702                matches!(
703                    &self.payload,
704                    Some(Payload::BondPayoutRequest(_)) | Some(Payload::PaymentRequest(_, _, _))
705                )
706            }
707            Action::AdminSettle | Action::AdminCancel => {
708                if self.id.is_none() {
709                    return false;
710                }
711                matches!(&self.payload, None | Some(Payload::BondResolution(_)))
712            }
713            Action::TakeSell
714            | Action::TakeBuy
715            | Action::FiatSent
716            | Action::FiatSentOk
717            | Action::Release
718            | Action::Released
719            | Action::Dispute
720            | Action::AdminCanceled
721            | Action::AdminSettled
722            | Action::Rate
723            | Action::RateReceived
724            | Action::AdminTakeDispute
725            | Action::AdminTookDispute
726            | Action::DisputeInitiatedByYou
727            | Action::DisputeInitiatedByPeer
728            | Action::WaitingBuyerInvoice
729            | Action::PurchaseCompleted
730            | Action::BondPayoutCompleted
731            | Action::HoldInvoicePaymentAccepted
732            | Action::HoldInvoicePaymentSettled
733            | Action::HoldInvoicePaymentCanceled
734            | Action::WaitingSellerToPay
735            | Action::BuyerTookOrder
736            | Action::BuyerInvoiceAccepted
737            | Action::BondInvoiceAccepted
738            | Action::CooperativeCancelInitiatedByYou
739            | Action::CooperativeCancelInitiatedByPeer
740            | Action::CooperativeCancelAccepted
741            | Action::Cancel
742            | Action::InvoiceUpdated
743            | Action::AdminAddSolver
744            | Action::SendDm
745            | Action::TradePubkey
746            | Action::Canceled => {
747                if self.id.is_none() {
748                    return false;
749                }
750                !matches!(
751                    &self.payload,
752                    Some(Payload::BondResolution(_)) | Some(Payload::BondPayoutRequest(_))
753                )
754            }
755            Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
756            Action::PaymentFailed => {
757                if self.id.is_none() {
758                    return false;
759                }
760                matches!(&self.payload, Some(Payload::PaymentFailed(_)))
761            }
762            Action::RateUser => {
763                matches!(&self.payload, Some(Payload::RatingUser(_)))
764            }
765            Action::CantDo => {
766                matches!(&self.payload, Some(Payload::CantDo(_)))
767            }
768            Action::Orders => {
769                matches!(
770                    &self.payload,
771                    Some(Payload::Ids(_)) | Some(Payload::Orders(_))
772                )
773            }
774        }
775    }
776
777    /// Return the [`SmallOrder`] carried by a [`Action::NewOrder`] message.
778    ///
779    /// Yields `None` if the action is not `NewOrder` or the payload is of a
780    /// different variant.
781    pub fn get_order(&self) -> Option<&SmallOrder> {
782        if self.action != Action::NewOrder {
783            return None;
784        }
785        match &self.payload {
786            Some(Payload::Order(o)) => Some(o),
787            _ => None,
788        }
789    }
790
791    /// Return the Lightning payment request embedded in a message.
792    ///
793    /// Valid only for [`Action::TakeSell`], [`Action::AddInvoice`],
794    /// [`Action::AddBondInvoice`] and [`Action::NewOrder`]. For `NewOrder`,
795    /// the invoice is read from the [`SmallOrder::buyer_invoice`] field.
796    /// Returns `None` otherwise.
797    pub fn get_payment_request(&self) -> Option<String> {
798        if self.action != Action::TakeSell
799            && self.action != Action::AddInvoice
800            && self.action != Action::AddBondInvoice
801            && self.action != Action::NewOrder
802        {
803            return None;
804        }
805        match &self.payload {
806            Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
807            Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
808            _ => None,
809        }
810    }
811
812    /// Return the amount override embedded in a [`Action::TakeSell`] or
813    /// [`Action::TakeBuy`] message, either from a [`Payload::Amount`] or
814    /// from the third element of a [`Payload::PaymentRequest`].
815    pub fn get_amount(&self) -> Option<Amount> {
816        if self.action != Action::TakeSell && self.action != Action::TakeBuy {
817            return None;
818        }
819        match &self.payload {
820            Some(Payload::PaymentRequest(_, _, amount)) => *amount,
821            Some(Payload::Amount(amount)) => Some(*amount),
822            _ => None,
823        }
824    }
825
826    /// Borrow the optional payload.
827    pub fn get_payload(&self) -> Option<&Payload> {
828        self.payload.as_ref()
829    }
830
831    /// Return `(true, index)` when the message carries a trade index,
832    /// `(false, 0)` otherwise.
833    pub fn has_trade_index(&self) -> (bool, i64) {
834        if let Some(index) = self.trade_index {
835            return (true, index);
836        }
837        (false, 0)
838    }
839
840    /// Return the trade index carried by the message, or `0` when absent.
841    pub fn trade_index(&self) -> i64 {
842        if let Some(index) = self.trade_index {
843            return index;
844        }
845        0
846    }
847}
848
849#[cfg(test)]
850mod test {
851    use crate::message::{Action, BondPayoutRequest, Message, MessageKind, Payload, Peer};
852    use crate::order::SmallOrder;
853    use crate::user::UserInfo;
854    use nostr_sdk::Keys;
855    use uuid::uuid;
856
857    #[test]
858    fn test_peer_with_reputation() {
859        // Test creating a Peer with reputation information
860        let reputation = UserInfo {
861            rating: 4.5,
862            reviews: 10,
863            operating_days: 30,
864        };
865        let peer = Peer::new(
866            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
867            Some(reputation.clone()),
868        );
869
870        // Assert the fields are set correctly
871        assert_eq!(
872            peer.pubkey,
873            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
874        );
875        assert!(peer.reputation.is_some());
876        let peer_reputation = peer.reputation.clone().unwrap();
877        assert_eq!(peer_reputation.rating, 4.5);
878        assert_eq!(peer_reputation.reviews, 10);
879        assert_eq!(peer_reputation.operating_days, 30);
880
881        // Test JSON serialization and deserialization
882        let json = peer.as_json().unwrap();
883        let deserialized_peer = Peer::from_json(&json).unwrap();
884        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
885        assert!(deserialized_peer.reputation.is_some());
886        let deserialized_reputation = deserialized_peer.reputation.unwrap();
887        assert_eq!(deserialized_reputation.rating, 4.5);
888        assert_eq!(deserialized_reputation.reviews, 10);
889        assert_eq!(deserialized_reputation.operating_days, 30);
890    }
891
892    #[test]
893    fn test_peer_without_reputation() {
894        // Test creating a Peer without reputation information
895        let peer = Peer::new(
896            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
897            None,
898        );
899
900        // Assert the reputation field is None
901        assert_eq!(
902            peer.pubkey,
903            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
904        );
905        assert!(peer.reputation.is_none());
906
907        // Test JSON serialization and deserialization
908        let json = peer.as_json().unwrap();
909        let deserialized_peer = Peer::from_json(&json).unwrap();
910        assert_eq!(deserialized_peer.pubkey, peer.pubkey);
911        assert!(deserialized_peer.reputation.is_none());
912    }
913
914    #[test]
915    fn test_peer_in_message() {
916        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
917
918        // Test with reputation
919        let reputation = UserInfo {
920            rating: 4.5,
921            reviews: 10,
922            operating_days: 30,
923        };
924        let peer_with_reputation = Peer::new(
925            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
926            Some(reputation),
927        );
928        let payload_with_reputation = Payload::Peer(peer_with_reputation);
929        let message_with_reputation = Message::Order(MessageKind::new(
930            Some(uuid),
931            Some(1),
932            Some(2),
933            Action::FiatSentOk,
934            Some(payload_with_reputation),
935        ));
936
937        // Verify message with reputation
938        assert!(message_with_reputation.verify());
939        let message_json = message_with_reputation.as_json().unwrap();
940        let deserialized_message = Message::from_json(&message_json).unwrap();
941        assert!(deserialized_message.verify());
942
943        // Test without reputation
944        let peer_without_reputation = Peer::new(
945            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
946            None,
947        );
948        let payload_without_reputation = Payload::Peer(peer_without_reputation);
949        let message_without_reputation = Message::Order(MessageKind::new(
950            Some(uuid),
951            Some(1),
952            Some(2),
953            Action::FiatSentOk,
954            Some(payload_without_reputation),
955        ));
956
957        // Verify message without reputation
958        assert!(message_without_reputation.verify());
959        let message_json = message_without_reputation.as_json().unwrap();
960        let deserialized_message = Message::from_json(&message_json).unwrap();
961        assert!(deserialized_message.verify());
962    }
963
964    #[test]
965    fn test_bond_payout_request_payload_verifies_on_add_bond_invoice() {
966        let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
967        let order = SmallOrder {
968            id: Some(order_id),
969            kind: None,
970            status: None,
971            amount: 500,
972            fiat_code: "USD".to_string(),
973            min_amount: None,
974            max_amount: None,
975            fiat_amount: 0,
976            payment_method: "lightning".to_string(),
977            premium: 0,
978            buyer_trade_pubkey: None,
979            seller_trade_pubkey: None,
980            buyer_invoice: None,
981            created_at: None,
982            expires_at: None,
983        };
984        let payload = Payload::BondPayoutRequest(BondPayoutRequest {
985            order,
986            slashed_at: 1_734_000_000,
987        });
988        let kind = MessageKind::new(
989            Some(order_id),
990            None,
991            None,
992            Action::AddBondInvoice,
993            Some(payload),
994        );
995        assert!(
996            kind.verify(),
997            "BondPayoutRequest must verify on AddBondInvoice"
998        );
999
1000        // Round-trip through JSON to catch a serde-rename mismatch on the
1001        // new snake_case discriminator.
1002        let m = Message::Order(kind);
1003        let json = m.as_json().unwrap();
1004        assert!(json.contains("bond_payout_request"));
1005        let back = Message::from_json(&json).unwrap();
1006        assert!(back.verify());
1007    }
1008
1009    #[test]
1010    fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1011        // BondPayoutRequest on any action other than AddBondInvoice must
1012        // fail verification — the new variant is opt-in per action.
1013        let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1014        let order = SmallOrder {
1015            id: Some(order_id),
1016            kind: None,
1017            status: None,
1018            amount: 500,
1019            fiat_code: "USD".to_string(),
1020            min_amount: None,
1021            max_amount: None,
1022            fiat_amount: 0,
1023            payment_method: "lightning".to_string(),
1024            premium: 0,
1025            buyer_trade_pubkey: None,
1026            seller_trade_pubkey: None,
1027            buyer_invoice: None,
1028            created_at: None,
1029            expires_at: None,
1030        };
1031
1032        // Compile-time exhaustiveness guard: adding a new Action variant
1033        // forces this match to be updated, which in turn forces a
1034        // decision about whether the new variant belongs in
1035        // `other_actions` below.
1036        let _exhaustive: fn(Action) = |a| match a {
1037            Action::AddBondInvoice => {}
1038            Action::NewOrder
1039            | Action::TakeSell
1040            | Action::TakeBuy
1041            | Action::PayInvoice
1042            | Action::PayBondInvoice
1043            | Action::FiatSent
1044            | Action::FiatSentOk
1045            | Action::Release
1046            | Action::Released
1047            | Action::Cancel
1048            | Action::Canceled
1049            | Action::CooperativeCancelInitiatedByYou
1050            | Action::CooperativeCancelInitiatedByPeer
1051            | Action::DisputeInitiatedByYou
1052            | Action::DisputeInitiatedByPeer
1053            | Action::CooperativeCancelAccepted
1054            | Action::BuyerInvoiceAccepted
1055            | Action::BondInvoiceAccepted
1056            | Action::PurchaseCompleted
1057            | Action::BondPayoutCompleted
1058            | Action::HoldInvoicePaymentAccepted
1059            | Action::HoldInvoicePaymentSettled
1060            | Action::HoldInvoicePaymentCanceled
1061            | Action::WaitingSellerToPay
1062            | Action::WaitingBuyerInvoice
1063            | Action::AddInvoice
1064            | Action::BuyerTookOrder
1065            | Action::Rate
1066            | Action::RateUser
1067            | Action::RateReceived
1068            | Action::CantDo
1069            | Action::Dispute
1070            | Action::AdminCancel
1071            | Action::AdminCanceled
1072            | Action::AdminSettle
1073            | Action::AdminSettled
1074            | Action::AdminAddSolver
1075            | Action::AdminTakeDispute
1076            | Action::AdminTookDispute
1077            | Action::PaymentFailed
1078            | Action::InvoiceUpdated
1079            | Action::SendDm
1080            | Action::TradePubkey
1081            | Action::RestoreSession
1082            | Action::LastTradeIndex
1083            | Action::Orders => {}
1084        };
1085
1086        let other_actions: &[Action] = &[
1087            Action::NewOrder,
1088            Action::TakeSell,
1089            Action::TakeBuy,
1090            Action::PayInvoice,
1091            Action::PayBondInvoice,
1092            Action::FiatSent,
1093            Action::FiatSentOk,
1094            Action::Release,
1095            Action::Released,
1096            Action::Cancel,
1097            Action::Canceled,
1098            Action::CooperativeCancelInitiatedByYou,
1099            Action::CooperativeCancelInitiatedByPeer,
1100            Action::DisputeInitiatedByYou,
1101            Action::DisputeInitiatedByPeer,
1102            Action::CooperativeCancelAccepted,
1103            Action::BuyerInvoiceAccepted,
1104            Action::BondInvoiceAccepted,
1105            Action::PurchaseCompleted,
1106            Action::BondPayoutCompleted,
1107            Action::HoldInvoicePaymentAccepted,
1108            Action::HoldInvoicePaymentSettled,
1109            Action::HoldInvoicePaymentCanceled,
1110            Action::WaitingSellerToPay,
1111            Action::WaitingBuyerInvoice,
1112            Action::AddInvoice,
1113            Action::BuyerTookOrder,
1114            Action::Rate,
1115            Action::RateUser,
1116            Action::RateReceived,
1117            Action::CantDo,
1118            Action::Dispute,
1119            Action::AdminCancel,
1120            Action::AdminCanceled,
1121            Action::AdminSettle,
1122            Action::AdminSettled,
1123            Action::AdminAddSolver,
1124            Action::AdminTakeDispute,
1125            Action::AdminTookDispute,
1126            Action::PaymentFailed,
1127            Action::InvoiceUpdated,
1128            Action::SendDm,
1129            Action::TradePubkey,
1130            Action::RestoreSession,
1131            Action::LastTradeIndex,
1132            Action::Orders,
1133        ];
1134
1135        for action in other_actions {
1136            let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1137                order: order.clone(),
1138                slashed_at: 0,
1139            });
1140            let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1141            assert!(
1142                !kind.verify(),
1143                "BondPayoutRequest must be rejected on {action:?}"
1144            );
1145        }
1146    }
1147
1148    #[test]
1149    fn test_bond_payout_ack_actions_verify_and_wire_format() {
1150        use crate::message::BondResolution;
1151
1152        let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1153
1154        // SmallOrder whose `amount` is the counterparty share carried by
1155        // these Mostro → winner bond-payout acknowledgements.
1156        let order = || SmallOrder {
1157            id: Some(order_id),
1158            kind: None,
1159            status: None,
1160            amount: 500,
1161            fiat_code: "USD".to_string(),
1162            min_amount: None,
1163            max_amount: None,
1164            fiat_amount: 0,
1165            payment_method: "lightning".to_string(),
1166            premium: 0,
1167            buyer_trade_pubkey: None,
1168            seller_trade_pubkey: None,
1169            buyer_invoice: None,
1170            created_at: None,
1171            expires_at: None,
1172        };
1173
1174        // Both variants are the bond duals of BuyerInvoiceAccepted /
1175        // PurchaseCompleted: id required, Payload::Order accepted, and the
1176        // bond request/resolution payloads rejected.
1177        for (action, discriminator) in [
1178            (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1179            (Action::BondPayoutCompleted, "bond-payout-completed"),
1180        ] {
1181            // id set + Payload::Order verifies.
1182            let ok = Message::Order(MessageKind::new(
1183                Some(order_id),
1184                Some(1),
1185                Some(2),
1186                action.clone(),
1187                Some(Payload::Order(order())),
1188            ));
1189            assert!(ok.verify(), "{action:?} + Order should verify");
1190
1191            // Missing id is invalid.
1192            let no_id = Message::Order(MessageKind::new(
1193                None,
1194                Some(1),
1195                Some(2),
1196                action.clone(),
1197                Some(Payload::Order(order())),
1198            ));
1199            assert!(!no_id.verify(), "{action:?} without id must be rejected");
1200
1201            // BondResolution payload is rejected on these outbound acks.
1202            let with_resolution = Message::Order(MessageKind::new(
1203                Some(order_id),
1204                Some(1),
1205                Some(2),
1206                action.clone(),
1207                Some(Payload::BondResolution(BondResolution {
1208                    slash_seller: true,
1209                    slash_buyer: false,
1210                })),
1211            ));
1212            assert!(
1213                !with_resolution.verify(),
1214                "{action:?} + BondResolution must be rejected"
1215            );
1216
1217            // BondPayoutRequest payload is rejected too.
1218            let with_request = Message::Order(MessageKind::new(
1219                Some(order_id),
1220                Some(1),
1221                Some(2),
1222                action.clone(),
1223                Some(Payload::BondPayoutRequest(BondPayoutRequest {
1224                    order: order(),
1225                    slashed_at: 0,
1226                })),
1227            ));
1228            assert!(
1229                !with_request.verify(),
1230                "{action:?} + BondPayoutRequest must be rejected"
1231            );
1232
1233            // Wire format uses the kebab-case discriminator and round-trips.
1234            let json = ok.as_json().unwrap();
1235            assert!(
1236                json.contains(&format!("\"action\":\"{discriminator}\"")),
1237                "expected kebab-case discriminator {discriminator}, got: {json}"
1238            );
1239            let decoded = Message::from_json(&json).unwrap();
1240            assert!(decoded.verify());
1241            assert_eq!(decoded.inner_action(), Some(action));
1242        }
1243    }
1244
1245    #[test]
1246    fn test_payment_failed_payload() {
1247        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1248
1249        // Test PaymentFailedInfo serialization and deserialization
1250        let payment_failed_info = crate::message::PaymentFailedInfo {
1251            payment_attempts: 3,
1252            payment_retries_interval: 60,
1253        };
1254
1255        let payload = Payload::PaymentFailed(payment_failed_info);
1256        let message = Message::Order(MessageKind::new(
1257            Some(uuid),
1258            Some(1),
1259            Some(2),
1260            Action::PaymentFailed,
1261            Some(payload),
1262        ));
1263
1264        // Verify message validation
1265        assert!(message.verify());
1266
1267        // Test JSON serialization
1268        let message_json = message.as_json().unwrap();
1269
1270        // Test deserialization
1271        let deserialized_message = Message::from_json(&message_json).unwrap();
1272        assert!(deserialized_message.verify());
1273
1274        // Verify the payload contains correct values
1275        if let Message::Order(kind) = deserialized_message {
1276            if let Some(Payload::PaymentFailed(info)) = kind.payload {
1277                assert_eq!(info.payment_attempts, 3);
1278                assert_eq!(info.payment_retries_interval, 60);
1279            } else {
1280                panic!("Expected PaymentFailed payload");
1281            }
1282        } else {
1283            panic!("Expected Order message");
1284        }
1285    }
1286
1287    #[test]
1288    fn test_message_payload_signature() {
1289        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1290        let peer = Peer::new(
1291            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1292            None, // Add None for the reputation parameter
1293        );
1294        let payload = Payload::Peer(peer);
1295        let test_message = Message::Order(MessageKind::new(
1296            Some(uuid),
1297            Some(1),
1298            Some(2),
1299            Action::FiatSentOk,
1300            Some(payload),
1301        ));
1302        assert!(test_message.verify());
1303        let test_message_json = test_message.as_json().unwrap();
1304        // Message should be signed with the trade keys
1305        let trade_keys =
1306            Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1307                .unwrap();
1308        let sig = Message::sign(test_message_json.clone(), &trade_keys);
1309
1310        assert!(Message::verify_signature(
1311            test_message_json,
1312            trade_keys.public_key(),
1313            sig
1314        ));
1315    }
1316
1317    #[test]
1318    fn test_restore_session_message() {
1319        // Test RestoreSession request (payload = None)
1320        let restore_request_message = Message::Restore(MessageKind::new(
1321            None,
1322            None,
1323            None,
1324            Action::RestoreSession,
1325            None,
1326        ));
1327
1328        // Verify message validation
1329        assert!(restore_request_message.verify());
1330        assert_eq!(
1331            restore_request_message.inner_action(),
1332            Some(Action::RestoreSession)
1333        );
1334
1335        // Test JSON serialization and deserialization for RestoreRequest
1336        let message_json = restore_request_message.as_json().unwrap();
1337        let deserialized_message = Message::from_json(&message_json).unwrap();
1338        assert!(deserialized_message.verify());
1339        assert_eq!(
1340            deserialized_message.inner_action(),
1341            Some(Action::RestoreSession)
1342        );
1343
1344        // Test RestoreSession with RestoreData payload
1345        let restored_orders = vec![
1346            crate::message::RestoredOrdersInfo {
1347                order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1348                trade_index: 1,
1349                status: "active".to_string(),
1350            },
1351            crate::message::RestoredOrdersInfo {
1352                order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1353                trade_index: 2,
1354                status: "success".to_string(),
1355            },
1356        ];
1357
1358        let restored_disputes = vec![
1359            crate::message::RestoredDisputesInfo {
1360                dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1361                order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1362                trade_index: 1,
1363                status: "initiated".to_string(),
1364                initiator: Some(crate::message::DisputeInitiator::Buyer),
1365                solver_pubkey: None,
1366            },
1367            crate::message::RestoredDisputesInfo {
1368                dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1369                order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1370                trade_index: 2,
1371                status: "in-progress".to_string(),
1372                initiator: None,
1373                solver_pubkey: Some(
1374                    "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1375                ),
1376            },
1377            crate::message::RestoredDisputesInfo {
1378                dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1379                order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1380                trade_index: 3,
1381                status: "initiated".to_string(),
1382                initiator: Some(crate::message::DisputeInitiator::Seller),
1383                solver_pubkey: None,
1384            },
1385        ];
1386
1387        let restore_session_info = crate::message::RestoreSessionInfo {
1388            restore_orders: restored_orders.clone(),
1389            restore_disputes: restored_disputes.clone(),
1390        };
1391
1392        let restore_data_payload = Payload::RestoreData(restore_session_info);
1393        let restore_data_message = Message::Restore(MessageKind::new(
1394            None,
1395            None,
1396            None,
1397            Action::RestoreSession,
1398            Some(restore_data_payload),
1399        ));
1400
1401        // With new logic, any payload for RestoreSession is invalid (must be None)
1402        assert!(!restore_data_message.verify());
1403
1404        // Verify serialization/deserialization of RestoreData payload with all initiator cases
1405        let message_json = restore_data_message.as_json().unwrap();
1406        let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1407
1408        if let Message::Restore(kind) = deserialized_restore_message {
1409            if let Some(Payload::RestoreData(session_info)) = kind.payload {
1410                assert_eq!(session_info.restore_disputes.len(), 3);
1411                assert_eq!(
1412                    session_info.restore_disputes[0].initiator,
1413                    Some(crate::message::DisputeInitiator::Buyer)
1414                );
1415                assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1416                assert_eq!(session_info.restore_disputes[1].initiator, None);
1417                assert_eq!(
1418                    session_info.restore_disputes[1].solver_pubkey,
1419                    Some(
1420                        "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1421                            .to_string()
1422                    )
1423                );
1424                assert_eq!(
1425                    session_info.restore_disputes[2].initiator,
1426                    Some(crate::message::DisputeInitiator::Seller)
1427                );
1428                assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1429            } else {
1430                panic!("Expected RestoreData payload");
1431            }
1432        } else {
1433            panic!("Expected Restore message");
1434        }
1435    }
1436
1437    #[test]
1438    fn test_restore_session_message_validation() {
1439        // Test that RestoreSession action accepts only payload=None or RestoreData
1440        let restore_request_message = Message::Restore(MessageKind::new(
1441            None,
1442            None,
1443            None,
1444            Action::RestoreSession,
1445            None, // Missing payload
1446        ));
1447
1448        // Verify restore request message
1449        assert!(restore_request_message.verify());
1450
1451        // Test with wrong payload type
1452        let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1453        let wrong_message = Message::Restore(MessageKind::new(
1454            None,
1455            None,
1456            None,
1457            Action::RestoreSession,
1458            Some(wrong_payload),
1459        ));
1460
1461        // Should fail validation because RestoreSession only accepts None
1462        assert!(!wrong_message.verify());
1463
1464        // With new logic, presence of id/request_id/trade_index is allowed
1465        let with_id = Message::Restore(MessageKind::new(
1466            Some(uuid!("00000000-0000-0000-0000-000000000001")),
1467            None,
1468            None,
1469            Action::RestoreSession,
1470            None,
1471        ));
1472        assert!(with_id.verify());
1473
1474        let with_request_id = Message::Restore(MessageKind::new(
1475            None,
1476            Some(42),
1477            None,
1478            Action::RestoreSession,
1479            None,
1480        ));
1481        assert!(with_request_id.verify());
1482
1483        let with_trade_index = Message::Restore(MessageKind::new(
1484            None,
1485            None,
1486            Some(7),
1487            Action::RestoreSession,
1488            None,
1489        ));
1490        assert!(with_trade_index.verify());
1491    }
1492
1493    #[test]
1494    fn test_restore_session_message_constructor() {
1495        // Test the new_restore constructor
1496        let restore_request_message = Message::new_restore(None);
1497
1498        assert!(matches!(restore_request_message, Message::Restore(_)));
1499        assert!(restore_request_message.verify());
1500        assert_eq!(
1501            restore_request_message.inner_action(),
1502            Some(Action::RestoreSession)
1503        );
1504
1505        // Test with RestoreData payload should be invalid now
1506        let restore_session_info = crate::message::RestoreSessionInfo {
1507            restore_orders: vec![],
1508            restore_disputes: vec![],
1509        };
1510        let restore_data_message =
1511            Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1512
1513        assert!(matches!(restore_data_message, Message::Restore(_)));
1514        assert!(!restore_data_message.verify());
1515    }
1516
1517    #[test]
1518    fn test_last_trade_index_valid_message() {
1519        let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1520        let msg = Message::Restore(kind);
1521
1522        assert!(msg.verify());
1523
1524        // roundtrip
1525        let json = msg.as_json().unwrap();
1526        let decoded = Message::from_json(&json).unwrap();
1527        assert!(decoded.verify());
1528
1529        // ensure the trade index is propagated
1530        let inner = decoded.get_inner_message_kind();
1531        assert_eq!(inner.trade_index(), 7);
1532        assert_eq!(inner.has_trade_index(), (true, 7));
1533    }
1534
1535    #[test]
1536    fn test_last_trade_index_without_id_is_valid() {
1537        // With new logic, id is not required; only payload must be None
1538        let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1539        let msg = Message::Restore(kind);
1540        assert!(msg.verify());
1541    }
1542
1543    #[test]
1544    fn test_last_trade_index_with_payload_fails_validation() {
1545        // LastTradeIndex does not accept payload
1546        let kind = MessageKind::new(
1547            None,
1548            None,
1549            Some(3),
1550            Action::LastTradeIndex,
1551            Some(Payload::TextMessage("ignored".to_string())),
1552        );
1553        let msg = Message::Restore(kind);
1554        assert!(!msg.verify());
1555    }
1556
1557    #[test]
1558    fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1559        use crate::message::BondResolution;
1560
1561        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1562
1563        for action in [Action::AdminSettle, Action::AdminCancel] {
1564            let with_resolution = Message::Order(MessageKind::new(
1565                Some(uuid),
1566                Some(1),
1567                Some(2),
1568                action.clone(),
1569                Some(Payload::BondResolution(BondResolution {
1570                    slash_seller: true,
1571                    slash_buyer: false,
1572                })),
1573            ));
1574            assert!(
1575                with_resolution.verify(),
1576                "{action:?} + BondResolution should verify"
1577            );
1578
1579            let without_payload = Message::Order(MessageKind::new(
1580                Some(uuid),
1581                Some(1),
1582                Some(2),
1583                action.clone(),
1584                None,
1585            ));
1586            assert!(without_payload.verify(), "{action:?} + None should verify");
1587
1588            // Wrong payload type must be rejected for these admin actions.
1589            let wrong = Message::Order(MessageKind::new(
1590                Some(uuid),
1591                Some(1),
1592                Some(2),
1593                action.clone(),
1594                Some(Payload::TextMessage("nope".to_string())),
1595            ));
1596            assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1597
1598            // Missing id is still invalid.
1599            let no_id = Message::Order(MessageKind::new(
1600                None,
1601                Some(1),
1602                Some(2),
1603                action,
1604                Some(Payload::BondResolution(BondResolution {
1605                    slash_seller: false,
1606                    slash_buyer: false,
1607                })),
1608            ));
1609            assert!(!no_id.verify(), "admin action without id must be rejected");
1610        }
1611    }
1612
1613    #[test]
1614    fn test_bond_resolution_rejected_on_non_admin_actions() {
1615        use crate::message::BondResolution;
1616
1617        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1618        let payload = Payload::BondResolution(BondResolution {
1619            slash_seller: true,
1620            slash_buyer: true,
1621        });
1622
1623        // Every Action except AdminSettle / AdminCancel must reject a
1624        // BondResolution payload. Listed explicitly (no strum) so that adding
1625        // a new Action variant forces a compile-error reminder here.
1626        for action in [
1627            Action::NewOrder,
1628            Action::TakeSell,
1629            Action::TakeBuy,
1630            Action::PayInvoice,
1631            Action::PayBondInvoice,
1632            Action::FiatSent,
1633            Action::FiatSentOk,
1634            Action::Release,
1635            Action::Released,
1636            Action::Cancel,
1637            Action::Canceled,
1638            Action::CooperativeCancelInitiatedByYou,
1639            Action::CooperativeCancelInitiatedByPeer,
1640            Action::DisputeInitiatedByYou,
1641            Action::DisputeInitiatedByPeer,
1642            Action::CooperativeCancelAccepted,
1643            Action::BuyerInvoiceAccepted,
1644            Action::BondInvoiceAccepted,
1645            Action::PurchaseCompleted,
1646            Action::BondPayoutCompleted,
1647            Action::HoldInvoicePaymentAccepted,
1648            Action::HoldInvoicePaymentSettled,
1649            Action::HoldInvoicePaymentCanceled,
1650            Action::WaitingSellerToPay,
1651            Action::WaitingBuyerInvoice,
1652            Action::AddInvoice,
1653            Action::AddBondInvoice,
1654            Action::BuyerTookOrder,
1655            Action::Rate,
1656            Action::RateUser,
1657            Action::RateReceived,
1658            Action::CantDo,
1659            Action::Dispute,
1660            Action::AdminCanceled,
1661            Action::AdminSettled,
1662            Action::AdminAddSolver,
1663            Action::AdminTakeDispute,
1664            Action::AdminTookDispute,
1665            Action::PaymentFailed,
1666            Action::InvoiceUpdated,
1667            Action::SendDm,
1668            Action::TradePubkey,
1669            Action::RestoreSession,
1670            Action::LastTradeIndex,
1671            Action::Orders,
1672        ] {
1673            let msg = Message::Order(MessageKind::new(
1674                Some(uuid),
1675                Some(1),
1676                Some(2),
1677                action.clone(),
1678                Some(payload.clone()),
1679            ));
1680            assert!(
1681                !msg.verify(),
1682                "{action:?} must reject BondResolution payload"
1683            );
1684        }
1685    }
1686
1687    #[test]
1688    fn test_bond_resolution_wire_format() {
1689        use crate::message::BondResolution;
1690
1691        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1692        let msg = Message::Order(MessageKind::new(
1693            Some(uuid),
1694            None,
1695            None,
1696            Action::AdminCancel,
1697            Some(Payload::BondResolution(BondResolution {
1698                slash_seller: true,
1699                slash_buyer: false,
1700            })),
1701        ));
1702
1703        let json = msg.as_json().unwrap();
1704        // Variant discriminator must be the snake_case `bond_resolution`.
1705        assert!(
1706            json.contains("\"bond_resolution\""),
1707            "expected snake_case discriminator, got: {json}"
1708        );
1709        assert!(json.contains("\"slash_seller\":true"));
1710        assert!(json.contains("\"slash_buyer\":false"));
1711
1712        // Roundtrip preserves the variant.
1713        let decoded = Message::from_json(&json).unwrap();
1714        assert!(decoded.verify());
1715        if let Message::Order(kind) = decoded {
1716            match kind.payload {
1717                Some(Payload::BondResolution(b)) => {
1718                    assert!(b.slash_seller);
1719                    assert!(!b.slash_buyer);
1720                }
1721                other => panic!("expected BondResolution payload, got {other:?}"),
1722            }
1723        } else {
1724            panic!("expected Order message");
1725        }
1726    }
1727
1728    #[test]
1729    fn test_bond_resolution_legacy_null_payload() {
1730        // payload = null on AdminSettle/AdminCancel must keep verifying so
1731        // pre-BondResolution clients keep working (interpreted as "no slash"
1732        // by the server).
1733        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1734        let json = format!(
1735            r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
1736        );
1737        let msg = Message::from_json(&json).unwrap();
1738        assert!(msg.verify());
1739    }
1740
1741    #[test]
1742    fn test_pay_bond_invoice_wire_format_and_verify() {
1743        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1744        let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1745
1746        let msg = Message::Order(MessageKind::new(
1747            Some(uuid),
1748            Some(1),
1749            Some(2),
1750            Action::PayBondInvoice,
1751            Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1752        ));
1753        assert!(msg.verify());
1754
1755        // Wire format must use the kebab-case discriminator.
1756        let json = msg.as_json().unwrap();
1757        assert!(
1758            json.contains("\"action\":\"pay-bond-invoice\""),
1759            "expected kebab-case discriminator, got: {json}"
1760        );
1761
1762        // Roundtrip preserves the variant.
1763        let decoded = Message::from_json(&json).unwrap();
1764        assert!(decoded.verify());
1765        assert!(matches!(
1766            decoded.inner_action(),
1767            Some(Action::PayBondInvoice)
1768        ));
1769
1770        // Same id / payload constraints as PayInvoice: missing id is invalid.
1771        let no_id = Message::Order(MessageKind::new(
1772            None,
1773            Some(1),
1774            Some(2),
1775            Action::PayBondInvoice,
1776            Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1777        ));
1778        assert!(!no_id.verify());
1779
1780        // Wrong payload shape is rejected.
1781        let wrong_payload = Message::Order(MessageKind::new(
1782            Some(uuid),
1783            Some(1),
1784            Some(2),
1785            Action::PayBondInvoice,
1786            Some(Payload::TextMessage("nope".to_string())),
1787        ));
1788        assert!(!wrong_payload.verify());
1789
1790        // Missing payload is rejected (PaymentRequest is required).
1791        let no_payload = Message::Order(MessageKind::new(
1792            Some(uuid),
1793            Some(1),
1794            Some(2),
1795            Action::PayBondInvoice,
1796            None,
1797        ));
1798        assert!(!no_payload.verify());
1799    }
1800
1801    #[test]
1802    fn test_add_bond_invoice_wire_format_and_verify() {
1803        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1804        let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1805
1806        let msg = Message::Order(MessageKind::new(
1807            Some(uuid),
1808            Some(1),
1809            Some(2),
1810            Action::AddBondInvoice,
1811            Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1812        ));
1813        assert!(msg.verify());
1814
1815        // Wire format must use the kebab-case discriminator.
1816        let json = msg.as_json().unwrap();
1817        assert!(
1818            json.contains("\"action\":\"add-bond-invoice\""),
1819            "expected kebab-case discriminator, got: {json}"
1820        );
1821
1822        // Roundtrip preserves the variant.
1823        let decoded = Message::from_json(&json).unwrap();
1824        assert!(decoded.verify());
1825        assert!(matches!(
1826            decoded.inner_action(),
1827            Some(Action::AddBondInvoice)
1828        ));
1829
1830        // Same id / payload constraints as AddInvoice: missing id is invalid.
1831        let no_id = Message::Order(MessageKind::new(
1832            None,
1833            Some(1),
1834            Some(2),
1835            Action::AddBondInvoice,
1836            Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1837        ));
1838        assert!(!no_id.verify());
1839
1840        // Wrong payload shape is rejected.
1841        let wrong_payload = Message::Order(MessageKind::new(
1842            Some(uuid),
1843            Some(1),
1844            Some(2),
1845            Action::AddBondInvoice,
1846            Some(Payload::TextMessage("nope".to_string())),
1847        ));
1848        assert!(!wrong_payload.verify());
1849
1850        // Missing payload is rejected (PaymentRequest is required).
1851        let no_payload = Message::Order(MessageKind::new(
1852            Some(uuid),
1853            Some(1),
1854            Some(2),
1855            Action::AddBondInvoice,
1856            None,
1857        ));
1858        assert!(!no_payload.verify());
1859
1860        // get_payment_request must surface the bolt11 for AddBondInvoice.
1861        if let Message::Order(kind) = &msg {
1862            assert_eq!(kind.get_payment_request(), Some(bolt11));
1863        } else {
1864            panic!("expected Message::Order");
1865        }
1866    }
1867
1868    #[test]
1869    fn test_restored_dispute_helper_serialization_roundtrip() {
1870        use crate::message::RestoredDisputeHelper;
1871
1872        let helper = RestoredDisputeHelper {
1873            dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1874            order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1875            dispute_status: "initiated".to_string(),
1876            master_buyer_pubkey: Some("npub1buyerkey".to_string()),
1877            master_seller_pubkey: Some("npub1sellerkey".to_string()),
1878            trade_index_buyer: Some(1),
1879            trade_index_seller: Some(2),
1880            buyer_dispute: true,
1881            seller_dispute: false,
1882            solver_pubkey: None,
1883        };
1884
1885        let json = serde_json::to_string(&helper).unwrap();
1886        let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
1887
1888        assert_eq!(deserialized.dispute_id, helper.dispute_id);
1889        assert_eq!(deserialized.order_id, helper.order_id);
1890        assert_eq!(deserialized.dispute_status, helper.dispute_status);
1891        assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
1892        assert_eq!(
1893            deserialized.master_seller_pubkey,
1894            helper.master_seller_pubkey
1895        );
1896        assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
1897        assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
1898        assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
1899        assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
1900        assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
1901
1902        let helper_seller_dispute = RestoredDisputeHelper {
1903            dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1904            order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1905            dispute_status: "in-progress".to_string(),
1906            master_buyer_pubkey: None,
1907            master_seller_pubkey: None,
1908            trade_index_buyer: None,
1909            trade_index_seller: None,
1910            buyer_dispute: false,
1911            seller_dispute: true,
1912            solver_pubkey: Some(
1913                "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1914            ),
1915        };
1916
1917        let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
1918        let deserialized_seller: RestoredDisputeHelper =
1919            serde_json::from_str(&json_seller).unwrap();
1920
1921        assert_eq!(
1922            deserialized_seller.dispute_id,
1923            helper_seller_dispute.dispute_id
1924        );
1925        assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
1926        assert_eq!(
1927            deserialized_seller.dispute_status,
1928            helper_seller_dispute.dispute_status
1929        );
1930        assert_eq!(deserialized_seller.master_buyer_pubkey, None);
1931        assert_eq!(deserialized_seller.master_seller_pubkey, None);
1932        assert_eq!(deserialized_seller.trade_index_buyer, None);
1933        assert_eq!(deserialized_seller.trade_index_seller, None);
1934        assert!(!deserialized_seller.buyer_dispute);
1935        assert!(deserialized_seller.seller_dispute);
1936        assert_eq!(
1937            deserialized_seller.solver_pubkey,
1938            helper_seller_dispute.solver_pubkey
1939        );
1940    }
1941}