1use crate::prelude::*;
11use nostr::key::PublicKey;
12use nostr::types::Timestamp;
13use serde::{Deserialize, Serialize};
14#[cfg(feature = "sqlx")]
15use sqlx::FromRow;
16use std::{fmt::Display, str::FromStr};
17use uuid::Uuid;
18use wasm_bindgen::prelude::*;
19
20#[wasm_bindgen]
22#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
23#[serde(rename_all = "kebab-case")]
24pub enum Kind {
25 Buy,
27 Sell,
29}
30
31impl FromStr for Kind {
32 type Err = ();
33
34 fn from_str(kind: &str) -> std::result::Result<Self, Self::Err> {
38 match kind.to_lowercase().as_str() {
39 "buy" => std::result::Result::Ok(Self::Buy),
40 "sell" => std::result::Result::Ok(Self::Sell),
41 _ => Err(()),
42 }
43 }
44}
45
46impl Display for Kind {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Kind::Sell => write!(f, "sell"),
50 Kind::Buy => write!(f, "buy"),
51 }
52 }
53}
54
55#[wasm_bindgen]
60#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
61#[serde(rename_all = "kebab-case")]
62pub enum Status {
63 Active,
65 Canceled,
67 CanceledByAdmin,
69 SettledByAdmin,
71 CompletedByAdmin,
73 Dispute,
75 Expired,
77 FiatSent,
79 SettledHoldInvoice,
81 Pending,
83 Success,
85 WaitingBuyerInvoice,
87 WaitingPayment,
89 WaitingTakerBond,
94 CooperativelyCanceled,
96 InProgress,
98 WaitingMakerBond,
108}
109
110impl Display for Status {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 match self {
113 Status::Active => write!(f, "active"),
114 Status::Canceled => write!(f, "canceled"),
115 Status::CanceledByAdmin => write!(f, "canceled-by-admin"),
116 Status::SettledByAdmin => write!(f, "settled-by-admin"),
117 Status::CompletedByAdmin => write!(f, "completed-by-admin"),
118 Status::Dispute => write!(f, "dispute"),
119 Status::Expired => write!(f, "expired"),
120 Status::FiatSent => write!(f, "fiat-sent"),
121 Status::SettledHoldInvoice => write!(f, "settled-hold-invoice"),
122 Status::Pending => write!(f, "pending"),
123 Status::Success => write!(f, "success"),
124 Status::WaitingBuyerInvoice => write!(f, "waiting-buyer-invoice"),
125 Status::WaitingPayment => write!(f, "waiting-payment"),
126 Status::WaitingTakerBond => write!(f, "waiting-taker-bond"),
127 Status::WaitingMakerBond => write!(f, "waiting-maker-bond"),
128 Status::CooperativelyCanceled => write!(f, "cooperatively-canceled"),
129 Status::InProgress => write!(f, "in-progress"),
130 }
131 }
132}
133
134impl FromStr for Status {
135 type Err = ();
136 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
138 match s.to_lowercase().as_str() {
139 "active" => std::result::Result::Ok(Self::Active),
140 "canceled" => std::result::Result::Ok(Self::Canceled),
141 "canceled-by-admin" => std::result::Result::Ok(Self::CanceledByAdmin),
142 "settled-by-admin" => std::result::Result::Ok(Self::SettledByAdmin),
143 "completed-by-admin" => std::result::Result::Ok(Self::CompletedByAdmin),
144 "dispute" => std::result::Result::Ok(Self::Dispute),
145 "expired" => std::result::Result::Ok(Self::Expired),
146 "fiat-sent" => std::result::Result::Ok(Self::FiatSent),
147 "settled-hold-invoice" => std::result::Result::Ok(Self::SettledHoldInvoice),
148 "pending" => std::result::Result::Ok(Self::Pending),
149 "success" => std::result::Result::Ok(Self::Success),
150 "waiting-buyer-invoice" => std::result::Result::Ok(Self::WaitingBuyerInvoice),
151 "waiting-payment" => std::result::Result::Ok(Self::WaitingPayment),
152 "waiting-taker-bond" => std::result::Result::Ok(Self::WaitingTakerBond),
153 "waiting-maker-bond" => std::result::Result::Ok(Self::WaitingMakerBond),
154 "cooperatively-canceled" => std::result::Result::Ok(Self::CooperativelyCanceled),
155 "in-progress" => std::result::Result::Ok(Self::InProgress),
156 _ => Err(()),
157 }
158 }
159}
160#[cfg_attr(feature = "sqlx", derive(FromRow))]
169#[derive(Debug, Default, Deserialize, Serialize, Clone)]
170pub struct Order {
171 pub id: Uuid,
173 pub kind: String,
176 pub event_id: String,
178 pub hash: Option<String>,
180 pub preimage: Option<String>,
182 pub creator_pubkey: String,
184 pub cancel_initiator_pubkey: Option<String>,
186 pub buyer_pubkey: Option<String>,
188 pub master_buyer_pubkey: Option<String>,
191 pub seller_pubkey: Option<String>,
193 pub master_seller_pubkey: Option<String>,
196 pub status: String,
198 pub price_from_api: bool,
200 pub premium: i64,
202 pub payment_method: String,
204 pub amount: i64,
207 pub min_amount: Option<i64>,
209 pub max_amount: Option<i64>,
211 pub buyer_dispute: bool,
213 pub seller_dispute: bool,
215 pub buyer_cooperativecancel: bool,
217 pub seller_cooperativecancel: bool,
219 pub fee: i64,
221 pub routing_fee: i64,
223 pub dev_fee: i64,
225 pub dev_fee_paid: bool,
227 pub dev_fee_payment_hash: Option<String>,
229 pub fiat_code: String,
231 pub fiat_amount: i64,
233 pub buyer_invoice: Option<String>,
235 pub range_parent_id: Option<Uuid>,
237 pub invoice_held_at: i64,
239 pub taken_at: i64,
241 pub created_at: i64,
243 pub buyer_sent_rate: bool,
245 pub seller_sent_rate: bool,
247 pub failed_payment: bool,
249 pub payment_attempts: i64,
251 pub expires_at: i64,
253 pub trade_index_seller: Option<i64>,
255 pub trade_index_buyer: Option<i64>,
257 pub next_trade_pubkey: Option<String>,
260 pub next_trade_index: Option<i64>,
262 pub cashu_mint_url: Option<String>,
265 pub cashu_escrow_token: Option<String>,
268 pub cashu_escrow_locked_at: Option<i64>,
271}
272
273impl From<SmallOrder> for Order {
274 fn from(small_order: SmallOrder) -> Self {
275 Self {
276 id: Uuid::new_v4(),
277 kind: small_order
279 .kind
280 .map_or_else(|| Kind::Buy.to_string(), |k| k.to_string()),
281 status: small_order
282 .status
283 .map_or_else(|| Status::Active.to_string(), |s| s.to_string()),
284 amount: small_order.amount,
285 fiat_code: small_order.fiat_code,
286 min_amount: small_order.min_amount,
287 max_amount: small_order.max_amount,
288 fiat_amount: small_order.fiat_amount,
289 payment_method: small_order.payment_method,
290 premium: small_order.premium,
291 event_id: String::new(),
292 creator_pubkey: String::new(),
293 price_from_api: false,
294 fee: 0,
295 routing_fee: 0,
296 dev_fee: 0,
297 dev_fee_paid: false,
298 dev_fee_payment_hash: None,
299 invoice_held_at: 0,
300 taken_at: 0,
301 created_at: small_order.created_at.unwrap_or(0),
302 expires_at: small_order.expires_at.unwrap_or(0),
303 payment_attempts: 0,
304 cashu_mint_url: small_order.cashu_mint_url,
305 ..Default::default()
306 }
307 }
308}
309
310impl Order {
311 pub fn as_new_order(&self) -> SmallOrder {
317 let small_order = SmallOrder::new(
318 Some(self.id),
319 Some(Kind::from_str(&self.kind).unwrap()),
320 Some(Status::from_str(&self.status).unwrap()),
321 self.amount,
322 self.fiat_code.clone(),
323 self.min_amount,
324 self.max_amount,
325 self.fiat_amount,
326 self.payment_method.clone(),
327 self.premium,
328 None,
329 None,
330 self.buyer_invoice.clone(),
331 Some(self.created_at),
332 Some(self.expires_at),
333 );
334 SmallOrder {
335 cashu_mint_url: self.cashu_mint_url.clone(),
336 ..small_order
337 }
338 }
339 pub fn get_order_kind(&self) -> Result<Kind, ServiceError> {
344 if let Ok(kind) = Kind::from_str(&self.kind) {
345 Ok(kind)
346 } else {
347 Err(ServiceError::InvalidOrderKind)
348 }
349 }
350
351 pub fn get_order_status(&self) -> Result<Status, ServiceError> {
356 if let Ok(status) = Status::from_str(&self.status) {
357 Ok(status)
358 } else {
359 Err(ServiceError::InvalidOrderStatus)
360 }
361 }
362
363 pub fn check_status(&self, status: Status) -> Result<(), CantDoReason> {
368 match Status::from_str(&self.status) {
369 Ok(s) => match s == status {
370 true => Ok(()),
371 false => Err(CantDoReason::InvalidOrderStatus),
372 },
373 Err(_) => Err(CantDoReason::InvalidOrderStatus),
374 }
375 }
376
377 pub fn is_buy_order(&self) -> Result<(), CantDoReason> {
379 if self.kind != Kind::Buy.to_string() {
380 return Err(CantDoReason::InvalidOrderKind);
381 }
382 Ok(())
383 }
384 pub fn is_sell_order(&self) -> Result<(), CantDoReason> {
386 if self.kind != Kind::Sell.to_string() {
387 return Err(CantDoReason::InvalidOrderKind);
388 }
389 Ok(())
390 }
391
392 pub fn sent_from_maker(&self, sender: PublicKey) -> Result<(), CantDoReason> {
396 let sender = sender.to_string();
397 if self.creator_pubkey != sender {
398 return Err(CantDoReason::InvalidPubkey);
399 }
400 Ok(())
401 }
402
403 pub fn not_sent_from_maker(&self, sender: PublicKey) -> Result<(), CantDoReason> {
408 let sender = sender.to_string();
409 if self.creator_pubkey == sender {
410 return Err(CantDoReason::InvalidPubkey);
411 }
412 Ok(())
413 }
414
415 pub fn get_creator_pubkey(&self) -> Result<PublicKey, ServiceError> {
417 match PublicKey::from_str(self.creator_pubkey.as_ref()) {
418 Ok(pk) => Ok(pk),
419 Err(_) => Err(ServiceError::InvalidPubkey),
420 }
421 }
422
423 pub fn get_buyer_pubkey(&self) -> Result<PublicKey, ServiceError> {
428 if let Some(pk) = self.buyer_pubkey.as_ref() {
429 PublicKey::from_str(pk).map_err(|_| ServiceError::InvalidPubkey)
430 } else {
431 Err(ServiceError::InvalidPubkey)
432 }
433 }
434 pub fn get_seller_pubkey(&self) -> Result<PublicKey, ServiceError> {
439 if let Some(pk) = self.seller_pubkey.as_ref() {
440 PublicKey::from_str(pk).map_err(|_| ServiceError::InvalidPubkey)
441 } else {
442 Err(ServiceError::InvalidPubkey)
443 }
444 }
445 pub fn get_master_buyer_pubkey(&self) -> Result<PublicKey, ServiceError> {
447 if let Some(pk) = self.master_buyer_pubkey.as_ref() {
448 PublicKey::from_str(pk).map_err(|_| ServiceError::InvalidPubkey)
449 } else {
450 Err(ServiceError::InvalidPubkey)
451 }
452 }
453 pub fn get_master_seller_pubkey(&self) -> Result<PublicKey, ServiceError> {
455 if let Some(pk) = self.master_seller_pubkey.as_ref() {
456 PublicKey::from_str(pk).map_err(|_| ServiceError::InvalidPubkey)
457 } else {
458 Err(ServiceError::InvalidPubkey)
459 }
460 }
461
462 pub fn is_range_order(&self) -> bool {
465 self.min_amount.is_some() && self.max_amount.is_some()
466 }
467
468 pub fn count_failed_payment(&mut self, retries_number: i64) {
474 if !self.failed_payment {
475 self.failed_payment = true;
476 self.payment_attempts = 1;
477 } else if self.payment_attempts < retries_number {
478 self.payment_attempts += 1;
479 }
480 }
481
482 pub fn has_no_amount(&self) -> bool {
485 self.amount == 0
486 }
487
488 pub fn set_timestamp_now(&mut self) {
490 self.taken_at = Timestamp::now().as_secs() as i64
491 }
492
493 pub fn is_full_privacy_order(&self) -> Result<(Option<String>, Option<String>), ServiceError> {
502 let (mut normal_buyer_idkey, mut normal_seller_idkey) = (None, None);
503
504 let master_buyer_pubkey = self.get_master_buyer_pubkey().ok();
506 let master_seller_pubkey = self.get_master_seller_pubkey().ok();
507
508 if self.buyer_pubkey != master_buyer_pubkey.map(|pk| pk.to_string()) {
510 normal_buyer_idkey = master_buyer_pubkey.map(|pk| pk.to_string());
511 }
512
513 if self.seller_pubkey != master_seller_pubkey.map(|pk| pk.to_string()) {
515 normal_seller_idkey = master_seller_pubkey.map(|pk| pk.to_string());
516 }
517
518 Ok((normal_buyer_idkey, normal_seller_idkey))
519 }
520 pub fn setup_dispute(&mut self, is_buyer_dispute: bool) -> Result<(), CantDoReason> {
528 let is_seller_dispute = !is_buyer_dispute;
530
531 let mut update_seller_dispute = false;
533 let mut update_buyer_dispute = false;
534
535 if is_seller_dispute && !self.seller_dispute {
536 update_seller_dispute = true;
537 self.seller_dispute = update_seller_dispute;
538 } else if is_buyer_dispute && !self.buyer_dispute {
539 update_buyer_dispute = true;
540 self.buyer_dispute = update_buyer_dispute;
541 };
542 self.status = Status::Dispute.to_string();
544
545 if !update_buyer_dispute && !update_seller_dispute {
548 return Err(CantDoReason::DisputeCreationError);
549 }
550
551 Ok(())
552 }
553}
554
555#[derive(Debug, Default, Deserialize, Serialize, Clone)]
564#[serde(deny_unknown_fields)]
565pub struct SmallOrder {
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub id: Option<Uuid>,
569 pub kind: Option<Kind>,
571 pub status: Option<Status>,
573 pub amount: i64,
576 pub fiat_code: String,
578 pub min_amount: Option<i64>,
580 pub max_amount: Option<i64>,
582 pub fiat_amount: i64,
584 pub payment_method: String,
586 pub premium: i64,
588 #[serde(skip_serializing_if = "Option::is_none")]
590 pub buyer_trade_pubkey: Option<String>,
591 #[serde(skip_serializing_if = "Option::is_none")]
593 pub seller_trade_pubkey: Option<String>,
594 #[serde(skip_serializing_if = "Option::is_none")]
596 pub buyer_invoice: Option<String>,
597 pub created_at: Option<i64>,
599 pub expires_at: Option<i64>,
601 #[serde(skip_serializing_if = "Option::is_none")]
607 pub cashu_mint_url: Option<String>,
608}
609
610#[allow(dead_code)]
611impl SmallOrder {
612 #[allow(clippy::too_many_arguments)]
614 pub fn new(
615 id: Option<Uuid>,
616 kind: Option<Kind>,
617 status: Option<Status>,
618 amount: i64,
619 fiat_code: String,
620 min_amount: Option<i64>,
621 max_amount: Option<i64>,
622 fiat_amount: i64,
623 payment_method: String,
624 premium: i64,
625 buyer_trade_pubkey: Option<String>,
626 seller_trade_pubkey: Option<String>,
627 buyer_invoice: Option<String>,
628 created_at: Option<i64>,
629 expires_at: Option<i64>,
630 ) -> Self {
631 Self {
632 id,
633 kind,
634 status,
635 amount,
636 fiat_code,
637 min_amount,
638 max_amount,
639 fiat_amount,
640 payment_method,
641 premium,
642 buyer_trade_pubkey,
643 seller_trade_pubkey,
644 buyer_invoice,
645 created_at,
646 expires_at,
647 cashu_mint_url: None,
648 }
649 }
650 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
652 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
653 }
654
655 pub fn as_json(&self) -> Result<String, ServiceError> {
657 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
658 }
659
660 pub fn sats_amount(&self) -> String {
663 if self.amount == 0 {
664 "Market price".to_string()
665 } else {
666 self.amount.to_string()
667 }
668 }
669 pub fn check_fiat_amount(&self) -> Result<(), CantDoReason> {
673 if self.fiat_amount <= 0 {
674 return Err(CantDoReason::InvalidAmount);
675 }
676 Ok(())
677 }
678
679 pub fn check_amount(&self) -> Result<(), CantDoReason> {
686 if self.amount < 0 {
687 return Err(CantDoReason::InvalidAmount);
688 }
689 Ok(())
690 }
691
692 pub fn check_zero_amount_with_premium(&self) -> Result<(), CantDoReason> {
698 let premium = (self.premium != 0).then_some(self.premium);
699 let sats_amount = (self.amount != 0).then_some(self.amount);
700
701 if premium.is_some() && sats_amount.is_some() {
702 return Err(CantDoReason::InvalidParameters);
703 }
704 Ok(())
705 }
706
707 pub fn check_range_order_limits(&self, amounts: &mut Vec<i64>) -> Result<(), CantDoReason> {
715 if let (Some(min), Some(max)) = (self.min_amount, self.max_amount) {
717 if min < 0 || max < 0 {
718 return Err(CantDoReason::InvalidAmount);
719 }
720 if min >= max {
721 return Err(CantDoReason::InvalidAmount);
722 }
723 if self.amount != 0 {
724 return Err(CantDoReason::InvalidAmount);
725 }
726 amounts.clear();
727 amounts.push(min);
728 amounts.push(max);
729 }
730 Ok(())
731 }
732
733 pub fn check_fiat_currency(
740 &self,
741 fiat_currencies_accepted: &[String],
742 ) -> Result<(), CantDoReason> {
743 if !fiat_currencies_accepted.contains(&self.fiat_code)
744 && !fiat_currencies_accepted.is_empty()
745 {
746 return Err(CantDoReason::InvalidFiatCurrency);
747 }
748 Ok(())
749 }
750}
751
752impl From<Order> for SmallOrder {
753 fn from(order: Order) -> Self {
754 let id = Some(order.id);
755 let kind = Kind::from_str(&order.kind).unwrap();
756 let status = Status::from_str(&order.status).unwrap();
757 let amount = order.amount;
758 let fiat_code = order.fiat_code.clone();
759 let min_amount = order.min_amount;
760 let max_amount = order.max_amount;
761 let fiat_amount = order.fiat_amount;
762 let payment_method = order.payment_method.clone();
763 let premium = order.premium;
764 let buyer_trade_pubkey = order.buyer_pubkey.clone();
765 let seller_trade_pubkey = order.seller_pubkey.clone();
766 let buyer_invoice = order.buyer_invoice.clone();
767
768 Self {
769 id,
770 kind: Some(kind),
771 status: Some(status),
772 amount,
773 fiat_code,
774 min_amount,
775 max_amount,
776 fiat_amount,
777 payment_method,
778 premium,
779 buyer_trade_pubkey,
780 seller_trade_pubkey,
781 buyer_invoice,
782 created_at: Some(order.created_at),
783 expires_at: Some(order.expires_at),
784 cashu_mint_url: order.cashu_mint_url,
785 }
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792 use crate::error::CantDoReason;
793 use nostr_sdk::prelude::Keys;
794 use uuid::uuid;
795
796 #[test]
797 fn test_status_string() {
798 assert_eq!(Status::Active.to_string(), "active");
799 assert_eq!(Status::CompletedByAdmin.to_string(), "completed-by-admin");
800 assert_eq!(Status::FiatSent.to_string(), "fiat-sent");
801 assert_ne!(Status::Pending.to_string(), "Pending");
802 }
803
804 #[test]
805 fn test_status_waiting_taker_bond_roundtrip() {
806 assert_eq!(Status::WaitingTakerBond.to_string(), "waiting-taker-bond");
807 assert_eq!(
808 Status::from_str("waiting-taker-bond").unwrap(),
809 Status::WaitingTakerBond
810 );
811 let json = serde_json::to_string(&Status::WaitingTakerBond).unwrap();
813 assert_eq!(json, "\"waiting-taker-bond\"");
814 let back: Status = serde_json::from_str(&json).unwrap();
815 assert_eq!(back, Status::WaitingTakerBond);
816 }
817
818 #[test]
819 fn test_status_waiting_maker_bond_roundtrip() {
820 assert_eq!(Status::WaitingMakerBond.to_string(), "waiting-maker-bond");
821 assert_eq!(
822 Status::from_str("waiting-maker-bond").unwrap(),
823 Status::WaitingMakerBond
824 );
825 let json = serde_json::to_string(&Status::WaitingMakerBond).unwrap();
827 assert_eq!(json, "\"waiting-maker-bond\"");
828 let back: Status = serde_json::from_str(&json).unwrap();
829 assert_eq!(back, Status::WaitingMakerBond);
830 }
831
832 #[test]
833 fn test_kind_string() {
834 assert_ne!(Kind::Sell.to_string(), "active");
835 assert_eq!(Kind::Sell.to_string(), "sell");
836 assert_eq!(Kind::Buy.to_string(), "buy");
837 assert_ne!(Kind::Buy.to_string(), "active");
838 }
839
840 #[test]
841 fn test_order_message() {
842 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
843 let payment_methods = "SEPA,Bank transfer".to_string();
844 let payload = Payload::Order(SmallOrder::new(
845 Some(uuid),
846 Some(Kind::Sell),
847 Some(Status::Pending),
848 100,
849 "eur".to_string(),
850 None,
851 None,
852 100,
853 payment_methods,
854 1,
855 None,
856 None,
857 None,
858 Some(1627371434),
859 None,
860 ));
861
862 let test_message = Message::Order(MessageKind::new(
863 Some(uuid),
864 Some(1),
865 Some(2),
866 Action::NewOrder,
867 Some(payload),
868 ));
869 let test_message_json = test_message.as_json().unwrap();
870 let sample_message = r#"{"order":{"version":2,"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}}}}"#;
871 let message = Message::from_json(sample_message).unwrap();
872 assert!(message.verify());
873 let message_json = message.as_json().unwrap();
874 assert_eq!(message_json, test_message_json);
875 }
876
877 #[test]
878 fn test_payment_request_payload_message() {
879 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
880 let test_message = Message::Order(MessageKind::new(
881 Some(uuid),
882 Some(1),
883 Some(3),
884 Action::PayInvoice,
885 Some(Payload::PaymentRequest(
886 Some(SmallOrder::new(
887 Some(uuid),
888 Some(Kind::Sell),
889 Some(Status::WaitingPayment),
890 100,
891 "eur".to_string(),
892 None,
893 None,
894 100,
895 "Face to face".to_string(),
896 1,
897 None,
898 None,
899 None,
900 Some(1627371434),
901 None,
902 )),
903 "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string(),
904 None,
905 )),
906 ));
907 let sample_message = r#"{"order":{"version":2,"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]}}}"#;
908 let message = Message::from_json(sample_message).unwrap();
909 assert!(message.verify());
910 let message_json = message.as_json().unwrap();
911 let test_message_json = test_message.as_json().unwrap();
912 assert_eq!(message_json, test_message_json);
913 }
914
915 #[test]
916 fn test_message_payload_signature() {
917 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
918 let peer = Peer::new(
919 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
920 None,
921 );
922 let payload = Payload::Peer(peer);
923 let test_message = Message::Order(MessageKind::new(
924 Some(uuid),
925 Some(1),
926 Some(2),
927 Action::FiatSentOk,
928 Some(payload),
929 ));
930 assert!(test_message.verify());
931 let test_message_json = test_message.as_json().unwrap();
932 let trade_keys =
934 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
935 .unwrap();
936 let sig = Message::sign(test_message_json.clone(), &trade_keys);
937
938 assert!(Message::verify_signature(
939 test_message_json,
940 trade_keys.public_key(),
941 sig
942 ));
943 }
944
945 #[test]
946 fn test_cant_do_message_serialization() {
947 let reasons = vec![
949 CantDoReason::InvalidSignature,
950 CantDoReason::InvalidTradeIndex,
951 CantDoReason::InvalidAmount,
952 CantDoReason::InvalidInvoice,
953 CantDoReason::InvalidPaymentRequest,
954 CantDoReason::InvalidPeer,
955 CantDoReason::InvalidRating,
956 CantDoReason::InvalidTextMessage,
957 CantDoReason::InvalidOrderStatus,
958 CantDoReason::InvalidPubkey,
959 CantDoReason::InvalidParameters,
960 CantDoReason::OrderAlreadyCanceled,
961 CantDoReason::CantCreateUser,
962 CantDoReason::IsNotYourOrder,
963 CantDoReason::NotAllowedByStatus,
964 CantDoReason::OutOfRangeFiatAmount,
965 CantDoReason::OutOfRangeSatsAmount,
966 CantDoReason::IsNotYourDispute,
967 CantDoReason::NotFound,
968 CantDoReason::InvalidFiatCurrency,
969 CantDoReason::TooManyRequests,
970 ];
971
972 for reason in reasons {
973 let cant_do = Message::CantDo(MessageKind::new(
974 None,
975 None,
976 None,
977 Action::CantDo,
978 Some(Payload::CantDo(Some(reason.clone()))),
979 ));
980 let message = Message::from_json(&cant_do.as_json().unwrap()).unwrap();
981 assert!(message.verify());
982 assert_eq!(message.as_json().unwrap(), cant_do.as_json().unwrap());
983 }
984
985 let cant_do = Message::CantDo(MessageKind::new(
987 None,
988 None,
989 None,
990 Action::CantDo,
991 Some(Payload::CantDo(None)),
992 ));
993 let message = Message::from_json(&cant_do.as_json().unwrap()).unwrap();
994 assert!(message.verify());
995 assert_eq!(message.as_json().unwrap(), cant_do.as_json().unwrap());
996 }
997
998 #[test]
1001 fn test_check_fiat_amount_valid() {
1002 let order = SmallOrder::new(
1004 None,
1005 None,
1006 None,
1007 100,
1008 "VES".to_string(),
1009 None,
1010 None,
1011 500,
1012 "Bank".to_string(),
1013 1,
1014 None,
1015 None,
1016 None,
1017 None,
1018 None,
1019 );
1020 assert!(order.check_fiat_amount().is_ok());
1021 }
1022
1023 #[test]
1024 fn test_check_fiat_amount_zero() {
1025 let order = SmallOrder::new(
1026 None,
1027 None,
1028 None,
1029 100,
1030 "VES".to_string(),
1031 None,
1032 None,
1033 0,
1034 "Bank".to_string(),
1035 1,
1036 None,
1037 None,
1038 None,
1039 None,
1040 None,
1041 );
1042 let result = order.check_fiat_amount();
1043 assert!(result.is_err());
1044 assert_eq!(result.unwrap_err(), CantDoReason::InvalidAmount);
1045 }
1046
1047 #[test]
1048 fn test_check_fiat_amount_negative() {
1049 let order = SmallOrder::new(
1050 None,
1051 None,
1052 None,
1053 100,
1054 "VES".to_string(),
1055 None,
1056 None,
1057 -100,
1058 "Bank".to_string(),
1059 1,
1060 None,
1061 None,
1062 None,
1063 None,
1064 None,
1065 );
1066 let result = order.check_fiat_amount();
1067 assert!(result.is_err());
1068 assert_eq!(result.unwrap_err(), CantDoReason::InvalidAmount);
1069 }
1070
1071 #[test]
1074 fn test_check_amount_valid() {
1075 let order = SmallOrder::new(
1077 None,
1078 None,
1079 None,
1080 100000,
1081 "VES".to_string(),
1082 None,
1083 None,
1084 500,
1085 "Bank".to_string(),
1086 0,
1087 None,
1088 None,
1089 None,
1090 None,
1091 None,
1092 );
1093 assert!(order.check_amount().is_ok());
1094 }
1095
1096 #[test]
1097 fn test_check_amount_zero() {
1098 let order = SmallOrder::new(
1100 None,
1101 None,
1102 None,
1103 0,
1104 "VES".to_string(),
1105 None,
1106 None,
1107 500,
1108 "Bank".to_string(),
1109 0,
1110 None,
1111 None,
1112 None,
1113 None,
1114 None,
1115 );
1116 assert!(order.check_amount().is_ok());
1117 }
1118
1119 #[test]
1120 fn test_check_amount_negative() {
1121 let order = SmallOrder::new(
1123 None,
1124 None,
1125 None,
1126 -1000,
1127 "VES".to_string(),
1128 None,
1129 None,
1130 500,
1131 "Bank".to_string(),
1132 0,
1133 None,
1134 None,
1135 None,
1136 None,
1137 None,
1138 );
1139 let result = order.check_amount();
1140 assert!(result.is_err());
1141 assert_eq!(result.unwrap_err(), CantDoReason::InvalidAmount);
1142 }
1143
1144 const MINT: &str = "https://mint.example.com";
1145
1146 fn cashu_order() -> Order {
1147 Order {
1148 kind: Kind::Sell.to_string(),
1149 status: Status::Pending.to_string(),
1150 fiat_code: "USD".to_string(),
1151 fiat_amount: 100,
1152 payment_method: "SEPA".to_string(),
1153 cashu_mint_url: Some(MINT.to_string()),
1154 ..Default::default()
1155 }
1156 }
1157
1158 #[test]
1159 fn small_order_omits_cashu_mint_url_when_none() {
1160 let order = SmallOrder::default();
1163 let json = order.as_json().unwrap();
1164 assert!(!json.contains("cashu_mint_url"));
1165 }
1166
1167 #[test]
1168 fn small_order_parses_without_cashu_mint_url() {
1169 let json = r#"{"kind":"sell","status":"pending","amount":0,"fiat_code":"USD","min_amount":null,"max_amount":null,"fiat_amount":100,"payment_method":"SEPA","premium":0,"created_at":null,"expires_at":null}"#;
1170 let order = SmallOrder::from_json(json).unwrap();
1171 assert_eq!(order.cashu_mint_url, None);
1172 }
1173
1174 #[test]
1175 fn small_order_cashu_mint_url_roundtrips() {
1176 let order = SmallOrder {
1177 cashu_mint_url: Some(MINT.to_string()),
1178 ..SmallOrder::default()
1179 };
1180 let json = order.as_json().unwrap();
1181 assert!(json.contains(r#""cashu_mint_url":"https://mint.example.com""#));
1182 let back = SmallOrder::from_json(&json).unwrap();
1183 assert_eq!(back.cashu_mint_url.as_deref(), Some(MINT));
1184 }
1185
1186 #[test]
1187 fn order_from_small_order_keeps_cashu_mint_url() {
1188 let small = SmallOrder {
1189 cashu_mint_url: Some(MINT.to_string()),
1190 ..SmallOrder::default()
1191 };
1192 let order = Order::from(small);
1193 assert_eq!(order.cashu_mint_url.as_deref(), Some(MINT));
1194 }
1195
1196 #[test]
1197 fn small_order_from_order_keeps_cashu_mint_url() {
1198 let small = SmallOrder::from(cashu_order());
1199 assert_eq!(small.cashu_mint_url.as_deref(), Some(MINT));
1200 }
1201
1202 #[test]
1203 fn as_new_order_keeps_cashu_mint_url() {
1204 let small = cashu_order().as_new_order();
1207 assert_eq!(small.cashu_mint_url.as_deref(), Some(MINT));
1208 }
1209}