Skip to main content

mostro_core/
order.rs

1//! Orders and their lifecycle.
2//!
3//! [`Order`] is the database-backed record for a trade between a buyer and a
4//! seller on Mostro. Orders have a [`Kind`] (buy or sell) and a [`Status`]
5//! that evolves through a small state machine as the trade progresses.
6//!
7//! [`SmallOrder`] is a compact, wire-friendly view of an order used when
8//! broadcasting via Nostr or surfacing minimal information to clients.
9
10use crate::prelude::*;
11use nostr_sdk::{PublicKey, Timestamp};
12use serde::{Deserialize, Serialize};
13#[cfg(feature = "sqlx")]
14use sqlx::FromRow;
15#[cfg(feature = "sqlx")]
16use sqlx_crud::SqlxCrud;
17use std::{fmt::Display, str::FromStr};
18use uuid::Uuid;
19use wasm_bindgen::prelude::*;
20
21/// Direction of an order: the maker wants to buy or sell sats.
22#[wasm_bindgen]
23#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
24#[serde(rename_all = "kebab-case")]
25pub enum Kind {
26    /// The maker wants to buy sats in exchange for fiat.
27    Buy,
28    /// The maker wants to sell sats in exchange for fiat.
29    Sell,
30}
31
32impl FromStr for Kind {
33    type Err = ();
34
35    /// Parse a [`Kind`] from `"buy"` or `"sell"` (case-insensitive).
36    ///
37    /// Returns `Err(())` for any other input.
38    fn from_str(kind: &str) -> std::result::Result<Self, Self::Err> {
39        match kind.to_lowercase().as_str() {
40            "buy" => std::result::Result::Ok(Self::Buy),
41            "sell" => std::result::Result::Ok(Self::Sell),
42            _ => Err(()),
43        }
44    }
45}
46
47impl Display for Kind {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            Kind::Sell => write!(f, "sell"),
51            Kind::Buy => write!(f, "buy"),
52        }
53    }
54}
55
56/// Lifecycle status of an [`Order`].
57///
58/// Values are serialized in `kebab-case`, matching the representation stored
59/// in the database and sent over the wire.
60#[wasm_bindgen]
61#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
62#[serde(rename_all = "kebab-case")]
63pub enum Status {
64    /// Order is published and available to be taken.
65    Active,
66    /// Order was canceled by the maker or the taker.
67    Canceled,
68    /// Order was canceled by an admin.
69    CanceledByAdmin,
70    /// Order was settled by an admin (solver) after a dispute.
71    SettledByAdmin,
72    /// Order was completed by an admin after a dispute.
73    CompletedByAdmin,
74    /// Order is currently in dispute.
75    Dispute,
76    /// Order expired before being taken or completed.
77    Expired,
78    /// Buyer has marked fiat as sent; waiting for the seller to release.
79    FiatSent,
80    /// Hold invoice has been settled; payment to the buyer is in flight.
81    SettledHoldInvoice,
82    /// Order has been created but not yet published.
83    Pending,
84    /// Trade completed successfully.
85    Success,
86    /// Waiting for the buyer's payout invoice.
87    WaitingBuyerInvoice,
88    /// Waiting for the seller to pay the hold invoice.
89    WaitingPayment,
90    /// Both parties agreed to cooperatively cancel the trade.
91    CooperativelyCanceled,
92    /// Order has been taken and the trade is in progress.
93    InProgress,
94}
95
96impl Display for Status {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        match self {
99            Status::Active => write!(f, "active"),
100            Status::Canceled => write!(f, "canceled"),
101            Status::CanceledByAdmin => write!(f, "canceled-by-admin"),
102            Status::SettledByAdmin => write!(f, "settled-by-admin"),
103            Status::CompletedByAdmin => write!(f, "completed-by-admin"),
104            Status::Dispute => write!(f, "dispute"),
105            Status::Expired => write!(f, "expired"),
106            Status::FiatSent => write!(f, "fiat-sent"),
107            Status::SettledHoldInvoice => write!(f, "settled-hold-invoice"),
108            Status::Pending => write!(f, "pending"),
109            Status::Success => write!(f, "success"),
110            Status::WaitingBuyerInvoice => write!(f, "waiting-buyer-invoice"),
111            Status::WaitingPayment => write!(f, "waiting-payment"),
112            Status::CooperativelyCanceled => write!(f, "cooperatively-canceled"),
113            Status::InProgress => write!(f, "in-progress"),
114        }
115    }
116}
117
118impl FromStr for Status {
119    type Err = ();
120    /// Convert a string to a status
121    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
122        match s.to_lowercase().as_str() {
123            "active" => std::result::Result::Ok(Self::Active),
124            "canceled" => std::result::Result::Ok(Self::Canceled),
125            "canceled-by-admin" => std::result::Result::Ok(Self::CanceledByAdmin),
126            "settled-by-admin" => std::result::Result::Ok(Self::SettledByAdmin),
127            "completed-by-admin" => std::result::Result::Ok(Self::CompletedByAdmin),
128            "dispute" => std::result::Result::Ok(Self::Dispute),
129            "expired" => std::result::Result::Ok(Self::Expired),
130            "fiat-sent" => std::result::Result::Ok(Self::FiatSent),
131            "settled-hold-invoice" => std::result::Result::Ok(Self::SettledHoldInvoice),
132            "pending" => std::result::Result::Ok(Self::Pending),
133            "success" => std::result::Result::Ok(Self::Success),
134            "waiting-buyer-invoice" => std::result::Result::Ok(Self::WaitingBuyerInvoice),
135            "waiting-payment" => std::result::Result::Ok(Self::WaitingPayment),
136            "cooperatively-canceled" => std::result::Result::Ok(Self::CooperativelyCanceled),
137            "in-progress" => std::result::Result::Ok(Self::InProgress),
138            _ => Err(()),
139        }
140    }
141}
142/// Persistent representation of a Mostro order.
143///
144/// This is the canonical on-disk record kept by a Mostro node. All fields
145/// are stored so an order can be recomputed / restarted from its row alone;
146/// clients usually work with the lighter [`SmallOrder`] view.
147///
148/// Timestamps are Unix seconds; `hash` / `preimage` refer to the hold
149/// invoice used to lock the seller's funds.
150#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud), external_id)]
151#[derive(Debug, Default, Deserialize, Serialize, Clone)]
152pub struct Order {
153    /// Unique order identifier.
154    pub id: Uuid,
155    /// Order kind ([`Kind::Buy`] or [`Kind::Sell`]), serialized as
156    /// kebab-case.
157    pub kind: String,
158    /// Nostr event id of the order publication.
159    pub event_id: String,
160    /// Payment hash of the seller's hold invoice, once generated.
161    pub hash: Option<String>,
162    /// Preimage revealed when the hold invoice is settled.
163    pub preimage: Option<String>,
164    /// Trade public key of the order creator (maker).
165    pub creator_pubkey: String,
166    /// Trade public key of the party who initiated a cancel, if any.
167    pub cancel_initiator_pubkey: Option<String>,
168    /// Buyer trade public key.
169    pub buyer_pubkey: Option<String>,
170    /// Buyer master identity pubkey. Equal to `buyer_pubkey` when the
171    /// buyer operates in full-privacy mode.
172    pub master_buyer_pubkey: Option<String>,
173    /// Seller trade public key.
174    pub seller_pubkey: Option<String>,
175    /// Seller master identity pubkey. Equal to `seller_pubkey` when the
176    /// seller operates in full-privacy mode.
177    pub master_seller_pubkey: Option<String>,
178    /// Current [`Status`] of the order, serialized as kebab-case.
179    pub status: String,
180    /// `true` if the sats amount was computed from a live market price.
181    pub price_from_api: bool,
182    /// Premium percentage applied on top of the spot price.
183    pub premium: i64,
184    /// Free-form payment method description (e.g. "SEPA,Bank transfer").
185    pub payment_method: String,
186    /// Sats amount. `0` means the amount is computed at take-time from the
187    /// fiat amount and the current market price.
188    pub amount: i64,
189    /// Lower bound of a range order (fiat amount). `None` for fixed orders.
190    pub min_amount: Option<i64>,
191    /// Upper bound of a range order (fiat amount). `None` for fixed orders.
192    pub max_amount: Option<i64>,
193    /// `true` when the buyer has initiated a dispute on this order.
194    pub buyer_dispute: bool,
195    /// `true` when the seller has initiated a dispute on this order.
196    pub seller_dispute: bool,
197    /// `true` when the buyer has initiated a cooperative cancel.
198    pub buyer_cooperativecancel: bool,
199    /// `true` when the seller has initiated a cooperative cancel.
200    pub seller_cooperativecancel: bool,
201    /// Mostro fee charged for this trade, in sats.
202    pub fee: i64,
203    /// Lightning routing fee observed when paying the buyer.
204    pub routing_fee: i64,
205    /// Optional developer-fee portion of `fee`.
206    pub dev_fee: i64,
207    /// `true` once the developer fee has been paid out.
208    pub dev_fee_paid: bool,
209    /// Payment hash of the developer-fee payment, when available.
210    pub dev_fee_payment_hash: Option<String>,
211    /// Fiat currency code (e.g. "EUR", "USD").
212    pub fiat_code: String,
213    /// Fiat amount of the trade.
214    pub fiat_amount: i64,
215    /// Buyer's Lightning payout invoice, once provided.
216    pub buyer_invoice: Option<String>,
217    /// Parent order id for orders derived from a range parent.
218    pub range_parent_id: Option<Uuid>,
219    /// Unix timestamp (seconds) when the hold invoice was locked in.
220    pub invoice_held_at: i64,
221    /// Unix timestamp (seconds) when the order was taken.
222    pub taken_at: i64,
223    /// Unix timestamp (seconds) when the order was created.
224    pub created_at: i64,
225    /// `true` once the buyer has rated the counterpart.
226    pub buyer_sent_rate: bool,
227    /// `true` once the seller has rated the counterpart.
228    pub seller_sent_rate: bool,
229    /// `true` if the latest payment attempt to the buyer failed.
230    pub failed_payment: bool,
231    /// Number of payment attempts performed so far.
232    pub payment_attempts: i64,
233    /// Unix timestamp (seconds) when the order expires automatically.
234    pub expires_at: i64,
235    /// Trade index used by the seller when creating / taking the order.
236    pub trade_index_seller: Option<i64>,
237    /// Trade index used by the buyer when creating / taking the order.
238    pub trade_index_buyer: Option<i64>,
239    /// Trade public key announced by a range-order maker for the next
240    /// trade in the same range.
241    pub next_trade_pubkey: Option<String>,
242    /// Trade index announced by a range-order maker for the next trade.
243    pub next_trade_index: Option<i64>,
244}
245
246impl From<SmallOrder> for Order {
247    fn from(small_order: SmallOrder) -> Self {
248        Self {
249            id: Uuid::new_v4(),
250            // order will be overwritten with the real one before publishing
251            kind: small_order
252                .kind
253                .map_or_else(|| Kind::Buy.to_string(), |k| k.to_string()),
254            status: small_order
255                .status
256                .map_or_else(|| Status::Active.to_string(), |s| s.to_string()),
257            amount: small_order.amount,
258            fiat_code: small_order.fiat_code,
259            min_amount: small_order.min_amount,
260            max_amount: small_order.max_amount,
261            fiat_amount: small_order.fiat_amount,
262            payment_method: small_order.payment_method,
263            premium: small_order.premium,
264            event_id: String::new(),
265            creator_pubkey: String::new(),
266            price_from_api: false,
267            fee: 0,
268            routing_fee: 0,
269            dev_fee: 0,
270            dev_fee_paid: false,
271            dev_fee_payment_hash: None,
272            invoice_held_at: 0,
273            taken_at: 0,
274            created_at: small_order.created_at.unwrap_or(0),
275            expires_at: small_order.expires_at.unwrap_or(0),
276            payment_attempts: 0,
277            ..Default::default()
278        }
279    }
280}
281
282impl Order {
283    /// Build a [`SmallOrder`] suitable for broadcasting as a new order event.
284    ///
285    /// Copies the tradable fields (amounts, payment method, premium, etc.)
286    /// from `self`. Trade pubkeys are left unset because a new order is
287    /// published before a counterpart is assigned.
288    pub fn as_new_order(&self) -> SmallOrder {
289        SmallOrder::new(
290            Some(self.id),
291            Some(Kind::from_str(&self.kind).unwrap()),
292            Some(Status::from_str(&self.status).unwrap()),
293            self.amount,
294            self.fiat_code.clone(),
295            self.min_amount,
296            self.max_amount,
297            self.fiat_amount,
298            self.payment_method.clone(),
299            self.premium,
300            None,
301            None,
302            self.buyer_invoice.clone(),
303            Some(self.created_at),
304            Some(self.expires_at),
305        )
306    }
307    /// Parse the order kind from the string-encoded field.
308    ///
309    /// Returns [`ServiceError::InvalidOrderKind`] when `self.kind` does not
310    /// match a known [`Kind`] variant.
311    pub fn get_order_kind(&self) -> Result<Kind, ServiceError> {
312        if let Ok(kind) = Kind::from_str(&self.kind) {
313            Ok(kind)
314        } else {
315            Err(ServiceError::InvalidOrderKind)
316        }
317    }
318
319    /// Parse the order status from the string-encoded field.
320    ///
321    /// Returns [`ServiceError::InvalidOrderStatus`] when `self.status` does
322    /// not match a known [`Status`] variant.
323    pub fn get_order_status(&self) -> Result<Status, ServiceError> {
324        if let Ok(status) = Status::from_str(&self.status) {
325            Ok(status)
326        } else {
327            Err(ServiceError::InvalidOrderStatus)
328        }
329    }
330
331    /// Check that the order is currently in a specific [`Status`].
332    ///
333    /// Returns `Ok(())` on match and [`CantDoReason::InvalidOrderStatus`]
334    /// either on mismatch or when the stored status cannot be parsed.
335    pub fn check_status(&self, status: Status) -> Result<(), CantDoReason> {
336        match Status::from_str(&self.status) {
337            Ok(s) => match s == status {
338                true => Ok(()),
339                false => Err(CantDoReason::InvalidOrderStatus),
340            },
341            Err(_) => Err(CantDoReason::InvalidOrderStatus),
342        }
343    }
344
345    /// Assert that the order is a [`Kind::Buy`] order.
346    pub fn is_buy_order(&self) -> Result<(), CantDoReason> {
347        if self.kind != Kind::Buy.to_string() {
348            return Err(CantDoReason::InvalidOrderKind);
349        }
350        Ok(())
351    }
352    /// Assert that the order is a [`Kind::Sell`] order.
353    pub fn is_sell_order(&self) -> Result<(), CantDoReason> {
354        if self.kind != Kind::Sell.to_string() {
355            return Err(CantDoReason::InvalidOrderKind);
356        }
357        Ok(())
358    }
359
360    /// Assert that `sender` is the maker (creator) of the order.
361    ///
362    /// Returns [`CantDoReason::InvalidPubkey`] when the pubkeys differ.
363    pub fn sent_from_maker(&self, sender: PublicKey) -> Result<(), CantDoReason> {
364        let sender = sender.to_string();
365        if self.creator_pubkey != sender {
366            return Err(CantDoReason::InvalidPubkey);
367        }
368        Ok(())
369    }
370
371    /// Assert that `sender` is **not** the maker of the order.
372    ///
373    /// Returns [`CantDoReason::InvalidPubkey`] when `sender` matches
374    /// `self.creator_pubkey`.
375    pub fn not_sent_from_maker(&self, sender: PublicKey) -> Result<(), CantDoReason> {
376        let sender = sender.to_string();
377        if self.creator_pubkey == sender {
378            return Err(CantDoReason::InvalidPubkey);
379        }
380        Ok(())
381    }
382
383    /// Parse the maker's public key as a Nostr [`PublicKey`].
384    pub fn get_creator_pubkey(&self) -> Result<PublicKey, ServiceError> {
385        match PublicKey::from_str(self.creator_pubkey.as_ref()) {
386            Ok(pk) => Ok(pk),
387            Err(_) => Err(ServiceError::InvalidPubkey),
388        }
389    }
390
391    /// Parse the buyer trade public key.
392    ///
393    /// Returns [`ServiceError::InvalidPubkey`] when the field is absent or
394    /// cannot be parsed.
395    pub fn get_buyer_pubkey(&self) -> Result<PublicKey, ServiceError> {
396        if let Some(pk) = self.buyer_pubkey.as_ref() {
397            PublicKey::from_str(pk).map_err(|_| ServiceError::InvalidPubkey)
398        } else {
399            Err(ServiceError::InvalidPubkey)
400        }
401    }
402    /// Parse the seller trade public key.
403    ///
404    /// Returns [`ServiceError::InvalidPubkey`] when the field is absent or
405    /// cannot be parsed.
406    pub fn get_seller_pubkey(&self) -> Result<PublicKey, ServiceError> {
407        if let Some(pk) = self.seller_pubkey.as_ref() {
408            PublicKey::from_str(pk).map_err(|_| ServiceError::InvalidPubkey)
409        } else {
410            Err(ServiceError::InvalidPubkey)
411        }
412    }
413    /// Parse the buyer master identity public key.
414    pub fn get_master_buyer_pubkey(&self) -> Result<PublicKey, ServiceError> {
415        if let Some(pk) = self.master_buyer_pubkey.as_ref() {
416            PublicKey::from_str(pk).map_err(|_| ServiceError::InvalidPubkey)
417        } else {
418            Err(ServiceError::InvalidPubkey)
419        }
420    }
421    /// Parse the seller master identity public key.
422    pub fn get_master_seller_pubkey(&self) -> Result<PublicKey, ServiceError> {
423        if let Some(pk) = self.master_seller_pubkey.as_ref() {
424            PublicKey::from_str(pk).map_err(|_| ServiceError::InvalidPubkey)
425        } else {
426            Err(ServiceError::InvalidPubkey)
427        }
428    }
429
430    /// `true` when both `min_amount` and `max_amount` are set, i.e. this is
431    /// a range order.
432    pub fn is_range_order(&self) -> bool {
433        self.min_amount.is_some() && self.max_amount.is_some()
434    }
435
436    /// Increment the payment-failure counter.
437    ///
438    /// On the first failure, sets [`Self::failed_payment`] to `true` and
439    /// [`Self::payment_attempts`] to `1`. On subsequent failures the counter
440    /// is bumped, capped at `retries_number`.
441    pub fn count_failed_payment(&mut self, retries_number: i64) {
442        if !self.failed_payment {
443            self.failed_payment = true;
444            self.payment_attempts = 1;
445        } else if self.payment_attempts < retries_number {
446            self.payment_attempts += 1;
447        }
448    }
449
450    /// `true` when `amount == 0`, meaning the sats amount is not fixed and
451    /// will be computed from the fiat amount and market price.
452    pub fn has_no_amount(&self) -> bool {
453        self.amount == 0
454    }
455
456    /// Set [`Self::taken_at`] to the current Unix timestamp.
457    pub fn set_timestamp_now(&mut self) {
458        self.taken_at = Timestamp::now().as_secs() as i64
459    }
460
461    /// Compare the trade pubkeys against the master pubkeys to detect which
462    /// sides of the trade are operating in full privacy mode.
463    ///
464    /// Returns a `(buyer_normal_idkey, seller_normal_idkey)` tuple. Each
465    /// value is `Some(master_pubkey)` when that side is running in normal
466    /// mode (trade key differs from master key, so the user is willing to
467    /// associate the trade with its reputation); `None` when the side is in
468    /// full privacy mode.
469    pub fn is_full_privacy_order(&self) -> Result<(Option<String>, Option<String>), ServiceError> {
470        let (mut normal_buyer_idkey, mut normal_seller_idkey) = (None, None);
471
472        // Get master pubkeys to get users data from db
473        let master_buyer_pubkey = self.get_master_buyer_pubkey().ok();
474        let master_seller_pubkey = self.get_master_seller_pubkey().ok();
475
476        // Check if the buyer is in full privacy mode
477        if self.buyer_pubkey != master_buyer_pubkey.map(|pk| pk.to_string()) {
478            normal_buyer_idkey = master_buyer_pubkey.map(|pk| pk.to_string());
479        }
480
481        // Check if the seller is in full privacy mode
482        if self.seller_pubkey != master_seller_pubkey.map(|pk| pk.to_string()) {
483            normal_seller_idkey = master_seller_pubkey.map(|pk| pk.to_string());
484        }
485
486        Ok((normal_buyer_idkey, normal_seller_idkey))
487    }
488    /// Mark the order as in dispute and record which side initiated it.
489    ///
490    /// When `is_buyer_dispute` is `true` the buyer flag is set, otherwise
491    /// the seller flag. The order status is then transitioned to
492    /// [`Status::Dispute`]. Returns
493    /// [`CantDoReason::DisputeCreationError`] when the appropriate flag was
494    /// already set (avoids registering the same dispute twice).
495    pub fn setup_dispute(&mut self, is_buyer_dispute: bool) -> Result<(), CantDoReason> {
496        // Get the opposite dispute status
497        let is_seller_dispute = !is_buyer_dispute;
498
499        // Update dispute flags based on who initiated
500        let mut update_seller_dispute = false;
501        let mut update_buyer_dispute = false;
502
503        if is_seller_dispute && !self.seller_dispute {
504            update_seller_dispute = true;
505            self.seller_dispute = update_seller_dispute;
506        } else if is_buyer_dispute && !self.buyer_dispute {
507            update_buyer_dispute = true;
508            self.buyer_dispute = update_buyer_dispute;
509        };
510        // Set the status to dispute
511        self.status = Status::Dispute.to_string();
512
513        // Update the database with dispute information
514        // Save the dispute to DB
515        if !update_buyer_dispute && !update_seller_dispute {
516            return Err(CantDoReason::DisputeCreationError);
517        }
518
519        Ok(())
520    }
521}
522
523/// Compact, wire-friendly view of an order.
524///
525/// `SmallOrder` carries the fields needed to publish a new order or to show
526/// a listing entry to a client, without the bookkeeping fields kept in
527/// [`Order`] (hold invoice hash, fees, dispute flags, etc.). It is the shape
528/// used by [`Payload::Order`] and siblings.
529///
530/// Unknown fields are rejected at deserialization time (`deny_unknown_fields`).
531#[derive(Debug, Default, Deserialize, Serialize, Clone)]
532#[serde(deny_unknown_fields)]
533pub struct SmallOrder {
534    /// Order id. `None` for orders that have not been persisted yet.
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub id: Option<Uuid>,
537    /// Order kind.
538    pub kind: Option<Kind>,
539    /// Current status.
540    pub status: Option<Status>,
541    /// Sats amount. `0` when the sats amount is derived from the fiat
542    /// amount and live market price.
543    pub amount: i64,
544    /// Fiat currency code (e.g. "EUR").
545    pub fiat_code: String,
546    /// Lower bound of a range order (fiat amount).
547    pub min_amount: Option<i64>,
548    /// Upper bound of a range order (fiat amount).
549    pub max_amount: Option<i64>,
550    /// Fiat amount of the trade.
551    pub fiat_amount: i64,
552    /// Free-form payment method description.
553    pub payment_method: String,
554    /// Premium percentage applied on top of the spot price.
555    pub premium: i64,
556    /// Buyer trade public key, when known.
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub buyer_trade_pubkey: Option<String>,
559    /// Seller trade public key, when known.
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub seller_trade_pubkey: Option<String>,
562    /// Buyer's Lightning payout invoice, when already provided.
563    #[serde(skip_serializing_if = "Option::is_none")]
564    pub buyer_invoice: Option<String>,
565    /// Unix timestamp (seconds) when the order was created.
566    pub created_at: Option<i64>,
567    /// Unix timestamp (seconds) when the order expires automatically.
568    pub expires_at: Option<i64>,
569}
570
571#[allow(dead_code)]
572impl SmallOrder {
573    /// Construct a new [`SmallOrder`] from all of its fields.
574    #[allow(clippy::too_many_arguments)]
575    pub fn new(
576        id: Option<Uuid>,
577        kind: Option<Kind>,
578        status: Option<Status>,
579        amount: i64,
580        fiat_code: String,
581        min_amount: Option<i64>,
582        max_amount: Option<i64>,
583        fiat_amount: i64,
584        payment_method: String,
585        premium: i64,
586        buyer_trade_pubkey: Option<String>,
587        seller_trade_pubkey: Option<String>,
588        buyer_invoice: Option<String>,
589        created_at: Option<i64>,
590        expires_at: Option<i64>,
591    ) -> Self {
592        Self {
593            id,
594            kind,
595            status,
596            amount,
597            fiat_code,
598            min_amount,
599            max_amount,
600            fiat_amount,
601            payment_method,
602            premium,
603            buyer_trade_pubkey,
604            seller_trade_pubkey,
605            buyer_invoice,
606            created_at,
607            expires_at,
608        }
609    }
610    /// Parse a [`SmallOrder`] from its JSON representation.
611    pub fn from_json(json: &str) -> Result<Self, ServiceError> {
612        serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
613    }
614
615    /// Serialize the order to a JSON string.
616    pub fn as_json(&self) -> Result<String, ServiceError> {
617        serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
618    }
619
620    /// Return the sats amount as a string, or the literal `"Market price"`
621    /// when the amount is `0` (to be computed at take-time).
622    pub fn sats_amount(&self) -> String {
623        if self.amount == 0 {
624            "Market price".to_string()
625        } else {
626            self.amount.to_string()
627        }
628    }
629    /// Assert that the fiat amount is strictly positive.
630    ///
631    /// Returns [`CantDoReason::InvalidAmount`] otherwise.
632    pub fn check_fiat_amount(&self) -> Result<(), CantDoReason> {
633        if self.fiat_amount <= 0 {
634            return Err(CantDoReason::InvalidAmount);
635        }
636        Ok(())
637    }
638
639    /// Assert that the sats amount is non-negative.
640    ///
641    /// A value of `0` is explicitly accepted because it signals that the
642    /// sats amount will be derived from the fiat amount and the market
643    /// price at take-time. Returns [`CantDoReason::InvalidAmount`] when the
644    /// amount is negative.
645    pub fn check_amount(&self) -> Result<(), CantDoReason> {
646        if self.amount < 0 {
647            return Err(CantDoReason::InvalidAmount);
648        }
649        Ok(())
650    }
651
652    /// Reject orders that set both `amount` and `premium` at the same time.
653    ///
654    /// A premium only makes sense when the sats amount is market-priced;
655    /// combining a fixed sats amount with a premium is ambiguous and
656    /// returns [`CantDoReason::InvalidParameters`].
657    pub fn check_zero_amount_with_premium(&self) -> Result<(), CantDoReason> {
658        let premium = (self.premium != 0).then_some(self.premium);
659        let sats_amount = (self.amount != 0).then_some(self.amount);
660
661        if premium.is_some() && sats_amount.is_some() {
662            return Err(CantDoReason::InvalidParameters);
663        }
664        Ok(())
665    }
666
667    /// Validate the bounds of a range order and push them into `amounts`.
668    ///
669    /// When both `min_amount` and `max_amount` are set, they must be
670    /// non-negative, `min < max`, and `amount` must be `0` (range orders
671    /// cannot fix the sats amount). On success, `amounts` is cleared and
672    /// replaced with `[min, max]`. On failure returns
673    /// [`CantDoReason::InvalidAmount`].
674    pub fn check_range_order_limits(&self, amounts: &mut Vec<i64>) -> Result<(), CantDoReason> {
675        // Check if the min and max amount are valid and update the vector
676        if let (Some(min), Some(max)) = (self.min_amount, self.max_amount) {
677            if min < 0 || max < 0 {
678                return Err(CantDoReason::InvalidAmount);
679            }
680            if min >= max {
681                return Err(CantDoReason::InvalidAmount);
682            }
683            if self.amount != 0 {
684                return Err(CantDoReason::InvalidAmount);
685            }
686            amounts.clear();
687            amounts.push(min);
688            amounts.push(max);
689        }
690        Ok(())
691    }
692
693    /// Verify that the order's fiat code appears in the list of accepted
694    /// currencies.
695    ///
696    /// An empty allowlist disables the check (every currency is accepted).
697    /// Returns [`CantDoReason::InvalidFiatCurrency`] when the currency is
698    /// not allowed.
699    pub fn check_fiat_currency(
700        &self,
701        fiat_currencies_accepted: &[String],
702    ) -> Result<(), CantDoReason> {
703        if !fiat_currencies_accepted.contains(&self.fiat_code)
704            && !fiat_currencies_accepted.is_empty()
705        {
706            return Err(CantDoReason::InvalidFiatCurrency);
707        }
708        Ok(())
709    }
710}
711
712impl From<Order> for SmallOrder {
713    fn from(order: Order) -> Self {
714        let id = Some(order.id);
715        let kind = Kind::from_str(&order.kind).unwrap();
716        let status = Status::from_str(&order.status).unwrap();
717        let amount = order.amount;
718        let fiat_code = order.fiat_code.clone();
719        let min_amount = order.min_amount;
720        let max_amount = order.max_amount;
721        let fiat_amount = order.fiat_amount;
722        let payment_method = order.payment_method.clone();
723        let premium = order.premium;
724        let buyer_trade_pubkey = order.buyer_pubkey.clone();
725        let seller_trade_pubkey = order.seller_pubkey.clone();
726        let buyer_invoice = order.buyer_invoice.clone();
727
728        Self {
729            id,
730            kind: Some(kind),
731            status: Some(status),
732            amount,
733            fiat_code,
734            min_amount,
735            max_amount,
736            fiat_amount,
737            payment_method,
738            premium,
739            buyer_trade_pubkey,
740            seller_trade_pubkey,
741            buyer_invoice,
742            created_at: Some(order.created_at),
743            expires_at: Some(order.expires_at),
744        }
745    }
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use crate::error::CantDoReason;
752    use nostr_sdk::Keys;
753    use uuid::uuid;
754
755    #[test]
756    fn test_status_string() {
757        assert_eq!(Status::Active.to_string(), "active");
758        assert_eq!(Status::CompletedByAdmin.to_string(), "completed-by-admin");
759        assert_eq!(Status::FiatSent.to_string(), "fiat-sent");
760        assert_ne!(Status::Pending.to_string(), "Pending");
761    }
762
763    #[test]
764    fn test_kind_string() {
765        assert_ne!(Kind::Sell.to_string(), "active");
766        assert_eq!(Kind::Sell.to_string(), "sell");
767        assert_eq!(Kind::Buy.to_string(), "buy");
768        assert_ne!(Kind::Buy.to_string(), "active");
769    }
770
771    #[test]
772    fn test_order_message() {
773        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
774        let payment_methods = "SEPA,Bank transfer".to_string();
775        let payload = Payload::Order(SmallOrder::new(
776            Some(uuid),
777            Some(Kind::Sell),
778            Some(Status::Pending),
779            100,
780            "eur".to_string(),
781            None,
782            None,
783            100,
784            payment_methods,
785            1,
786            None,
787            None,
788            None,
789            Some(1627371434),
790            None,
791        ));
792
793        let test_message = Message::Order(MessageKind::new(
794            Some(uuid),
795            Some(1),
796            Some(2),
797            Action::NewOrder,
798            Some(payload),
799        ));
800        let test_message_json = test_message.as_json().unwrap();
801        let sample_message = r#"{"order":{"version":1,"id":"308e1272-d5f4-47e6-bd97-3504baea9c23","request_id":1,"trade_index":2,"action":"new-order","payload":{"order":{"id":"308e1272-d5f4-47e6-bd97-3504baea9c23","kind":"sell","status":"pending","amount":100,"fiat_code":"eur","fiat_amount":100,"payment_method":"SEPA,Bank transfer","premium":1,"created_at":1627371434}}}}"#;
802        let message = Message::from_json(sample_message).unwrap();
803        assert!(message.verify());
804        let message_json = message.as_json().unwrap();
805        assert_eq!(message_json, test_message_json);
806    }
807
808    #[test]
809    fn test_payment_request_payload_message() {
810        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
811        let test_message = Message::Order(MessageKind::new(
812            Some(uuid),
813            Some(1),
814            Some(3),
815            Action::PayInvoice,
816            Some(Payload::PaymentRequest(
817                Some(SmallOrder::new(
818                    Some(uuid),
819                    Some(Kind::Sell),
820                    Some(Status::WaitingPayment),
821                    100,
822                    "eur".to_string(),
823                    None,
824                    None,
825                    100,
826                    "Face to face".to_string(),
827                    1,
828                    None,
829                    None,
830                    None,
831                    Some(1627371434),
832                    None,
833                )),
834                "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string(),
835                None,
836            )),
837        ));
838        let sample_message = r#"{"order":{"version":1,"id":"308e1272-d5f4-47e6-bd97-3504baea9c23","request_id":1,"trade_index":3,"action":"pay-invoice","payload":{"payment_request":[{"id":"308e1272-d5f4-47e6-bd97-3504baea9c23","kind":"sell","status":"waiting-payment","amount":100,"fiat_code":"eur","fiat_amount":100,"payment_method":"Face to face","premium":1,"created_at":1627371434},"lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e",null]}}}"#;
839        let message = Message::from_json(sample_message).unwrap();
840        assert!(message.verify());
841        let message_json = message.as_json().unwrap();
842        let test_message_json = test_message.as_json().unwrap();
843        assert_eq!(message_json, test_message_json);
844    }
845
846    #[test]
847    fn test_message_payload_signature() {
848        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
849        let peer = Peer::new(
850            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
851            None,
852        );
853        let payload = Payload::Peer(peer);
854        let test_message = Message::Order(MessageKind::new(
855            Some(uuid),
856            Some(1),
857            Some(2),
858            Action::FiatSentOk,
859            Some(payload),
860        ));
861        assert!(test_message.verify());
862        let test_message_json = test_message.as_json().unwrap();
863        // Message should be signed with the trade keys
864        let trade_keys =
865            Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
866                .unwrap();
867        let sig = Message::sign(test_message_json.clone(), &trade_keys);
868
869        assert!(Message::verify_signature(
870            test_message_json,
871            trade_keys.public_key(),
872            sig
873        ));
874    }
875
876    #[test]
877    fn test_cant_do_message_serialization() {
878        // Test all CantDoReason variants
879        let reasons = vec![
880            CantDoReason::InvalidSignature,
881            CantDoReason::InvalidTradeIndex,
882            CantDoReason::InvalidAmount,
883            CantDoReason::InvalidInvoice,
884            CantDoReason::InvalidPaymentRequest,
885            CantDoReason::InvalidPeer,
886            CantDoReason::InvalidRating,
887            CantDoReason::InvalidTextMessage,
888            CantDoReason::InvalidOrderStatus,
889            CantDoReason::InvalidPubkey,
890            CantDoReason::InvalidParameters,
891            CantDoReason::OrderAlreadyCanceled,
892            CantDoReason::CantCreateUser,
893            CantDoReason::IsNotYourOrder,
894            CantDoReason::NotAllowedByStatus,
895            CantDoReason::OutOfRangeFiatAmount,
896            CantDoReason::OutOfRangeSatsAmount,
897            CantDoReason::IsNotYourDispute,
898            CantDoReason::NotFound,
899            CantDoReason::InvalidFiatCurrency,
900            CantDoReason::TooManyRequests,
901        ];
902
903        for reason in reasons {
904            let cant_do = Message::CantDo(MessageKind::new(
905                None,
906                None,
907                None,
908                Action::CantDo,
909                Some(Payload::CantDo(Some(reason.clone()))),
910            ));
911            let message = Message::from_json(&cant_do.as_json().unwrap()).unwrap();
912            assert!(message.verify());
913            assert_eq!(message.as_json().unwrap(), cant_do.as_json().unwrap());
914        }
915
916        // Test None case
917        let cant_do = Message::CantDo(MessageKind::new(
918            None,
919            None,
920            None,
921            Action::CantDo,
922            Some(Payload::CantDo(None)),
923        ));
924        let message = Message::from_json(&cant_do.as_json().unwrap()).unwrap();
925        assert!(message.verify());
926        assert_eq!(message.as_json().unwrap(), cant_do.as_json().unwrap());
927    }
928
929    // === check_fiat_amount tests ===
930
931    #[test]
932    fn test_check_fiat_amount_valid() {
933        // id, kind, status, amount, fiat_code, min_amount, max_amount, fiat_amount, payment_method, premium, buyer_pubkey, seller_pubkey, buyer_invoice, created_at, expires_at
934        let order = SmallOrder::new(
935            None,
936            None,
937            None,
938            100,
939            "VES".to_string(),
940            None,
941            None,
942            500,
943            "Bank".to_string(),
944            1,
945            None,
946            None,
947            None,
948            None,
949            None,
950        );
951        assert!(order.check_fiat_amount().is_ok());
952    }
953
954    #[test]
955    fn test_check_fiat_amount_zero() {
956        let order = SmallOrder::new(
957            None,
958            None,
959            None,
960            100,
961            "VES".to_string(),
962            None,
963            None,
964            0,
965            "Bank".to_string(),
966            1,
967            None,
968            None,
969            None,
970            None,
971            None,
972        );
973        let result = order.check_fiat_amount();
974        assert!(result.is_err());
975        assert_eq!(result.unwrap_err(), CantDoReason::InvalidAmount);
976    }
977
978    #[test]
979    fn test_check_fiat_amount_negative() {
980        let order = SmallOrder::new(
981            None,
982            None,
983            None,
984            100,
985            "VES".to_string(),
986            None,
987            None,
988            -100,
989            "Bank".to_string(),
990            1,
991            None,
992            None,
993            None,
994            None,
995            None,
996        );
997        let result = order.check_fiat_amount();
998        assert!(result.is_err());
999        assert_eq!(result.unwrap_err(), CantDoReason::InvalidAmount);
1000    }
1001
1002    // === check_amount tests ===
1003
1004    #[test]
1005    fn test_check_amount_valid() {
1006        // amount = 100000 (positive, valid sats)
1007        let order = SmallOrder::new(
1008            None,
1009            None,
1010            None,
1011            100000,
1012            "VES".to_string(),
1013            None,
1014            None,
1015            500,
1016            "Bank".to_string(),
1017            0,
1018            None,
1019            None,
1020            None,
1021            None,
1022            None,
1023        );
1024        assert!(order.check_amount().is_ok());
1025    }
1026
1027    #[test]
1028    fn test_check_amount_zero() {
1029        // amount = 0 is valid (seller sets exact sats amount)
1030        let order = SmallOrder::new(
1031            None,
1032            None,
1033            None,
1034            0,
1035            "VES".to_string(),
1036            None,
1037            None,
1038            500,
1039            "Bank".to_string(),
1040            0,
1041            None,
1042            None,
1043            None,
1044            None,
1045            None,
1046        );
1047        assert!(order.check_amount().is_ok());
1048    }
1049
1050    #[test]
1051    fn test_check_amount_negative() {
1052        // amount = -1000 (negative, invalid)
1053        let order = SmallOrder::new(
1054            None,
1055            None,
1056            None,
1057            -1000,
1058            "VES".to_string(),
1059            None,
1060            None,
1061            500,
1062            "Bank".to_string(),
1063            0,
1064            None,
1065            None,
1066            None,
1067            None,
1068            None,
1069        );
1070        let result = order.check_amount();
1071        assert!(result.is_err());
1072        assert_eq!(result.unwrap_err(), CantDoReason::InvalidAmount);
1073    }
1074}