1use crate::prelude::*;
2use bitcoin::hashes::sha256::Hash as Sha256Hash;
3use bitcoin::hashes::Hash;
4use bitcoin::key::Secp256k1;
5use bitcoin::secp256k1::Message as BitcoinMessage;
6use nostr_sdk::prelude::*;
7#[cfg(feature = "sqlx")]
8use sqlx::FromRow;
9#[cfg(feature = "sqlx")]
10use sqlx_crud::SqlxCrud;
11
12use std::fmt;
13use uuid::Uuid;
14
15#[derive(Debug, Deserialize, Serialize, Clone)]
17pub struct Peer {
18 pub pubkey: String,
19 pub reputation: Option<UserInfo>,
20}
21
22impl Peer {
23 pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
24 Self { pubkey, reputation }
25 }
26
27 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
28 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
29 }
30
31 pub fn as_json(&self) -> Result<String, ServiceError> {
32 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
33 }
34}
35
36#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
38#[serde(rename_all = "kebab-case")]
39pub enum Action {
40 NewOrder,
41 TakeSell,
42 TakeBuy,
43 PayInvoice,
44 FiatSent,
45 FiatSentOk,
46 Release,
47 Released,
48 Cancel,
49 Canceled,
50 CooperativeCancelInitiatedByYou,
51 CooperativeCancelInitiatedByPeer,
52 DisputeInitiatedByYou,
53 DisputeInitiatedByPeer,
54 CooperativeCancelAccepted,
55 BuyerInvoiceAccepted,
56 PurchaseCompleted,
57 HoldInvoicePaymentAccepted,
58 HoldInvoicePaymentSettled,
59 HoldInvoicePaymentCanceled,
60 WaitingSellerToPay,
61 WaitingBuyerInvoice,
62 AddInvoice,
63 BuyerTookOrder,
64 Rate,
65 RateUser,
66 RateReceived,
67 CantDo,
68 Dispute,
69 AdminCancel,
70 AdminCanceled,
71 AdminSettle,
72 AdminSettled,
73 AdminAddSolver,
74 AdminTakeDispute,
75 AdminTookDispute,
76 PaymentFailed,
77 InvoiceUpdated,
78 SendDm,
79 TradePubkey,
80 RestoreSession,
81 LastTradeIndex,
82 Orders,
83}
84
85impl fmt::Display for Action {
86 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87 write!(f, "{self:?}")
88 }
89}
90
91#[derive(Debug, Clone, Deserialize, Serialize)]
93#[serde(rename_all = "kebab-case")]
94pub enum Message {
95 Order(MessageKind),
96 Dispute(MessageKind),
97 CantDo(MessageKind),
98 Rate(MessageKind),
99 Dm(MessageKind),
100 Restore(MessageKind),
101}
102
103impl Message {
104 pub fn new_order(
106 id: Option<Uuid>,
107 request_id: Option<u64>,
108 trade_index: Option<i64>,
109 action: Action,
110 payload: Option<Payload>,
111 ) -> Self {
112 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
113 Self::Order(kind)
114 }
115
116 pub fn new_dispute(
118 id: Option<Uuid>,
119 request_id: Option<u64>,
120 trade_index: Option<i64>,
121 action: Action,
122 payload: Option<Payload>,
123 ) -> Self {
124 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
125
126 Self::Dispute(kind)
127 }
128
129 pub fn new_restore(payload: Option<Payload>) -> Self {
130 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
131 Self::Restore(kind)
132 }
133
134 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
136 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
137
138 Self::CantDo(kind)
139 }
140
141 pub fn new_dm(
143 id: Option<Uuid>,
144 request_id: Option<u64>,
145 action: Action,
146 payload: Option<Payload>,
147 ) -> Self {
148 let kind = MessageKind::new(id, request_id, None, action, payload);
149
150 Self::Dm(kind)
151 }
152
153 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
155 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
156 }
157
158 pub fn as_json(&self) -> Result<String, ServiceError> {
160 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
161 }
162
163 pub fn get_inner_message_kind(&self) -> &MessageKind {
165 match self {
166 Message::Dispute(k)
167 | Message::Order(k)
168 | Message::CantDo(k)
169 | Message::Rate(k)
170 | Message::Dm(k)
171 | Message::Restore(k) => k,
172 }
173 }
174
175 pub fn inner_action(&self) -> Option<Action> {
177 match self {
178 Message::Dispute(a)
179 | Message::Order(a)
180 | Message::CantDo(a)
181 | Message::Rate(a)
182 | Message::Dm(a)
183 | Message::Restore(a) => Some(a.get_action()),
184 }
185 }
186
187 pub fn verify(&self) -> bool {
189 match self {
190 Message::Order(m)
191 | Message::Dispute(m)
192 | Message::CantDo(m)
193 | Message::Rate(m)
194 | Message::Dm(m)
195 | Message::Restore(m) => m.verify(),
196 }
197 }
198
199 pub fn sign(message: String, keys: &Keys) -> Signature {
200 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
201 let hash = hash.to_byte_array();
202 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
203
204 keys.sign_schnorr(&message)
205 }
206
207 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
208 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
210 let hash = hash.to_byte_array();
211 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
212
213 let secp = Secp256k1::verification_only();
215 if let Ok(xonlykey) = pubkey.xonly() {
217 xonlykey.verify(&secp, &message, &sig).is_ok()
218 } else {
219 false
220 }
221 }
222}
223
224#[derive(Debug, Clone, Deserialize, Serialize)]
226pub struct MessageKind {
227 pub version: u8,
229 pub request_id: Option<u64>,
231 pub trade_index: Option<i64>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub id: Option<Uuid>,
236 pub action: Action,
238 pub payload: Option<Payload>,
240}
241
242type Amount = i64;
243
244#[derive(Debug, Deserialize, Serialize, Clone)]
246pub struct PaymentFailedInfo {
247 pub payment_attempts: u32,
249 pub payment_retries_interval: u32,
251}
252
253#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
256#[derive(Debug, Deserialize, Serialize, Clone)]
257pub struct RestoredOrderHelper {
258 pub id: Uuid,
259 pub status: String,
260 pub master_buyer_pubkey: Option<String>,
261 pub master_seller_pubkey: Option<String>,
262 pub trade_index_buyer: Option<i64>,
263 pub trade_index_seller: Option<i64>,
264}
265
266#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
269#[derive(Debug, Deserialize, Serialize, Clone)]
270pub struct RestoredDisputeHelper {
271 pub dispute_id: Uuid,
272 pub order_id: Uuid,
273 pub dispute_status: String,
274 pub master_buyer_pubkey: Option<String>,
275 pub master_seller_pubkey: Option<String>,
276 pub trade_index_buyer: Option<i64>,
277 pub trade_index_seller: Option<i64>,
278 pub buyer_dispute: bool,
281 pub seller_dispute: bool,
284}
285
286#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
288#[derive(Debug, Deserialize, Serialize, Clone)]
289pub struct RestoredOrdersInfo {
290 pub order_id: Uuid,
292 pub trade_index: i64,
294 pub status: String,
296}
297
298#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
300#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
301#[serde(rename_all = "lowercase")]
302#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
303pub enum DisputeInitiator {
304 Buyer,
305 Seller,
306}
307
308#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
310#[derive(Debug, Deserialize, Serialize, Clone)]
311pub struct RestoredDisputesInfo {
312 pub dispute_id: Uuid,
314 pub order_id: Uuid,
316 pub trade_index: i64,
318 pub status: String,
320 pub initiator: Option<DisputeInitiator>,
322}
323
324#[derive(Debug, Deserialize, Serialize, Clone, Default)]
326pub struct RestoreSessionInfo {
327 #[serde(rename = "orders")]
329 pub restore_orders: Vec<RestoredOrdersInfo>,
330 #[serde(rename = "disputes")]
332 pub restore_disputes: Vec<RestoredDisputesInfo>,
333}
334
335#[derive(Debug, Deserialize, Serialize, Clone)]
337#[serde(rename_all = "snake_case")]
338pub enum Payload {
339 Order(SmallOrder),
341 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
343 TextMessage(String),
345 Peer(Peer),
347 RatingUser(u8),
349 Amount(Amount),
351 Dispute(Uuid, Option<SolverDisputeInfo>),
353 CantDo(Option<CantDoReason>),
355 NextTrade(String, u32),
359 PaymentFailed(PaymentFailedInfo),
361 RestoreData(RestoreSessionInfo),
363 Ids(Vec<Uuid>),
365 Orders(Vec<SmallOrder>),
367}
368
369#[allow(dead_code)]
370impl MessageKind {
371 pub fn new(
373 id: Option<Uuid>,
374 request_id: Option<u64>,
375 trade_index: Option<i64>,
376 action: Action,
377 payload: Option<Payload>,
378 ) -> Self {
379 Self {
380 version: PROTOCOL_VER,
381 request_id,
382 trade_index,
383 id,
384 action,
385 payload,
386 }
387 }
388 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
390 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
391 }
392 pub fn as_json(&self) -> Result<String, ServiceError> {
394 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
395 }
396
397 pub fn get_action(&self) -> Action {
399 self.action.clone()
400 }
401
402 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
404 match &self.payload {
405 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
406 None => Ok(None),
407 _ => Err(ServiceError::InvalidPayload),
408 }
409 }
410
411 pub fn get_rating(&self) -> Result<u8, ServiceError> {
412 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
413 if !(MIN_RATING..=MAX_RATING).contains(&v) {
414 return Err(ServiceError::InvalidRatingValue);
415 }
416 Ok(v)
417 } else {
418 Err(ServiceError::InvalidRating)
419 }
420 }
421
422 pub fn verify(&self) -> bool {
424 match &self.action {
425 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
426 Action::PayInvoice | Action::AddInvoice => {
427 if self.id.is_none() {
428 return false;
429 }
430 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
431 }
432 Action::TakeSell
433 | Action::TakeBuy
434 | Action::FiatSent
435 | Action::FiatSentOk
436 | Action::Release
437 | Action::Released
438 | Action::Dispute
439 | Action::AdminCancel
440 | Action::AdminCanceled
441 | Action::AdminSettle
442 | Action::AdminSettled
443 | Action::Rate
444 | Action::RateReceived
445 | Action::AdminTakeDispute
446 | Action::AdminTookDispute
447 | Action::DisputeInitiatedByYou
448 | Action::DisputeInitiatedByPeer
449 | Action::WaitingBuyerInvoice
450 | Action::PurchaseCompleted
451 | Action::HoldInvoicePaymentAccepted
452 | Action::HoldInvoicePaymentSettled
453 | Action::HoldInvoicePaymentCanceled
454 | Action::WaitingSellerToPay
455 | Action::BuyerTookOrder
456 | Action::BuyerInvoiceAccepted
457 | Action::CooperativeCancelInitiatedByYou
458 | Action::CooperativeCancelInitiatedByPeer
459 | Action::CooperativeCancelAccepted
460 | Action::Cancel
461 | Action::InvoiceUpdated
462 | Action::AdminAddSolver
463 | Action::SendDm
464 | Action::TradePubkey
465 | Action::Canceled => {
466 if self.id.is_none() {
467 return false;
468 }
469 true
470 }
471 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
472 Action::PaymentFailed => {
473 if self.id.is_none() {
474 return false;
475 }
476 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
477 }
478 Action::RateUser => {
479 matches!(&self.payload, Some(Payload::RatingUser(_)))
480 }
481 Action::CantDo => {
482 matches!(&self.payload, Some(Payload::CantDo(_)))
483 }
484 Action::Orders => {
485 matches!(
486 &self.payload,
487 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
488 )
489 }
490 }
491 }
492
493 pub fn get_order(&self) -> Option<&SmallOrder> {
494 if self.action != Action::NewOrder {
495 return None;
496 }
497 match &self.payload {
498 Some(Payload::Order(o)) => Some(o),
499 _ => None,
500 }
501 }
502
503 pub fn get_payment_request(&self) -> Option<String> {
504 if self.action != Action::TakeSell
505 && self.action != Action::AddInvoice
506 && self.action != Action::NewOrder
507 {
508 return None;
509 }
510 match &self.payload {
511 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
512 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
513 _ => None,
514 }
515 }
516
517 pub fn get_amount(&self) -> Option<Amount> {
518 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
519 return None;
520 }
521 match &self.payload {
522 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
523 Some(Payload::Amount(amount)) => Some(*amount),
524 _ => None,
525 }
526 }
527
528 pub fn get_payload(&self) -> Option<&Payload> {
529 self.payload.as_ref()
530 }
531
532 pub fn has_trade_index(&self) -> (bool, i64) {
533 if let Some(index) = self.trade_index {
534 return (true, index);
535 }
536 (false, 0)
537 }
538
539 pub fn trade_index(&self) -> i64 {
540 if let Some(index) = self.trade_index {
541 return index;
542 }
543 0
544 }
545}
546
547#[cfg(test)]
548mod test {
549 use crate::message::{Action, Message, MessageKind, Payload, Peer};
550 use crate::user::UserInfo;
551 use nostr_sdk::Keys;
552 use uuid::uuid;
553
554 #[test]
555 fn test_peer_with_reputation() {
556 let reputation = UserInfo {
558 rating: 4.5,
559 reviews: 10,
560 operating_days: 30,
561 };
562 let peer = Peer::new(
563 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
564 Some(reputation.clone()),
565 );
566
567 assert_eq!(
569 peer.pubkey,
570 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
571 );
572 assert!(peer.reputation.is_some());
573 let peer_reputation = peer.reputation.clone().unwrap();
574 assert_eq!(peer_reputation.rating, 4.5);
575 assert_eq!(peer_reputation.reviews, 10);
576 assert_eq!(peer_reputation.operating_days, 30);
577
578 let json = peer.as_json().unwrap();
580 let deserialized_peer = Peer::from_json(&json).unwrap();
581 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
582 assert!(deserialized_peer.reputation.is_some());
583 let deserialized_reputation = deserialized_peer.reputation.unwrap();
584 assert_eq!(deserialized_reputation.rating, 4.5);
585 assert_eq!(deserialized_reputation.reviews, 10);
586 assert_eq!(deserialized_reputation.operating_days, 30);
587 }
588
589 #[test]
590 fn test_peer_without_reputation() {
591 let peer = Peer::new(
593 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
594 None,
595 );
596
597 assert_eq!(
599 peer.pubkey,
600 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
601 );
602 assert!(peer.reputation.is_none());
603
604 let json = peer.as_json().unwrap();
606 let deserialized_peer = Peer::from_json(&json).unwrap();
607 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
608 assert!(deserialized_peer.reputation.is_none());
609 }
610
611 #[test]
612 fn test_peer_in_message() {
613 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
614
615 let reputation = UserInfo {
617 rating: 4.5,
618 reviews: 10,
619 operating_days: 30,
620 };
621 let peer_with_reputation = Peer::new(
622 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
623 Some(reputation),
624 );
625 let payload_with_reputation = Payload::Peer(peer_with_reputation);
626 let message_with_reputation = Message::Order(MessageKind::new(
627 Some(uuid),
628 Some(1),
629 Some(2),
630 Action::FiatSentOk,
631 Some(payload_with_reputation),
632 ));
633
634 assert!(message_with_reputation.verify());
636 let message_json = message_with_reputation.as_json().unwrap();
637 let deserialized_message = Message::from_json(&message_json).unwrap();
638 assert!(deserialized_message.verify());
639
640 let peer_without_reputation = Peer::new(
642 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
643 None,
644 );
645 let payload_without_reputation = Payload::Peer(peer_without_reputation);
646 let message_without_reputation = Message::Order(MessageKind::new(
647 Some(uuid),
648 Some(1),
649 Some(2),
650 Action::FiatSentOk,
651 Some(payload_without_reputation),
652 ));
653
654 assert!(message_without_reputation.verify());
656 let message_json = message_without_reputation.as_json().unwrap();
657 let deserialized_message = Message::from_json(&message_json).unwrap();
658 assert!(deserialized_message.verify());
659 }
660
661 #[test]
662 fn test_payment_failed_payload() {
663 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
664
665 let payment_failed_info = crate::message::PaymentFailedInfo {
667 payment_attempts: 3,
668 payment_retries_interval: 60,
669 };
670
671 let payload = Payload::PaymentFailed(payment_failed_info);
672 let message = Message::Order(MessageKind::new(
673 Some(uuid),
674 Some(1),
675 Some(2),
676 Action::PaymentFailed,
677 Some(payload),
678 ));
679
680 assert!(message.verify());
682
683 let message_json = message.as_json().unwrap();
685
686 let deserialized_message = Message::from_json(&message_json).unwrap();
688 assert!(deserialized_message.verify());
689
690 if let Message::Order(kind) = deserialized_message {
692 if let Some(Payload::PaymentFailed(info)) = kind.payload {
693 assert_eq!(info.payment_attempts, 3);
694 assert_eq!(info.payment_retries_interval, 60);
695 } else {
696 panic!("Expected PaymentFailed payload");
697 }
698 } else {
699 panic!("Expected Order message");
700 }
701 }
702
703 #[test]
704 fn test_message_payload_signature() {
705 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
706 let peer = Peer::new(
707 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
708 None, );
710 let payload = Payload::Peer(peer);
711 let test_message = Message::Order(MessageKind::new(
712 Some(uuid),
713 Some(1),
714 Some(2),
715 Action::FiatSentOk,
716 Some(payload),
717 ));
718 assert!(test_message.verify());
719 let test_message_json = test_message.as_json().unwrap();
720 let trade_keys =
722 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
723 .unwrap();
724 let sig = Message::sign(test_message_json.clone(), &trade_keys);
725
726 assert!(Message::verify_signature(
727 test_message_json,
728 trade_keys.public_key(),
729 sig
730 ));
731 }
732
733 #[test]
734 fn test_restore_session_message() {
735 let restore_request_message = Message::Restore(MessageKind::new(
737 None,
738 None,
739 None,
740 Action::RestoreSession,
741 None,
742 ));
743
744 assert!(restore_request_message.verify());
746 assert_eq!(
747 restore_request_message.inner_action(),
748 Some(Action::RestoreSession)
749 );
750
751 let message_json = restore_request_message.as_json().unwrap();
753 let deserialized_message = Message::from_json(&message_json).unwrap();
754 assert!(deserialized_message.verify());
755 assert_eq!(
756 deserialized_message.inner_action(),
757 Some(Action::RestoreSession)
758 );
759
760 let restored_orders = vec![
762 crate::message::RestoredOrdersInfo {
763 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
764 trade_index: 1,
765 status: "active".to_string(),
766 },
767 crate::message::RestoredOrdersInfo {
768 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
769 trade_index: 2,
770 status: "success".to_string(),
771 },
772 ];
773
774 let restored_disputes = vec![
775 crate::message::RestoredDisputesInfo {
776 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
777 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
778 trade_index: 1,
779 status: "initiated".to_string(),
780 initiator: Some(crate::message::DisputeInitiator::Buyer),
781 },
782 crate::message::RestoredDisputesInfo {
783 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
784 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
785 trade_index: 2,
786 status: "in-progress".to_string(),
787 initiator: None,
788 },
789 crate::message::RestoredDisputesInfo {
790 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
791 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
792 trade_index: 3,
793 status: "initiated".to_string(),
794 initiator: Some(crate::message::DisputeInitiator::Seller),
795 },
796 ];
797
798 let restore_session_info = crate::message::RestoreSessionInfo {
799 restore_orders: restored_orders.clone(),
800 restore_disputes: restored_disputes.clone(),
801 };
802
803 let restore_data_payload = Payload::RestoreData(restore_session_info);
804 let restore_data_message = Message::Restore(MessageKind::new(
805 None,
806 None,
807 None,
808 Action::RestoreSession,
809 Some(restore_data_payload),
810 ));
811
812 assert!(!restore_data_message.verify());
814
815 let message_json = restore_data_message.as_json().unwrap();
817 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
818
819 if let Message::Restore(kind) = deserialized_restore_message {
820 if let Some(Payload::RestoreData(session_info)) = kind.payload {
821 assert_eq!(session_info.restore_disputes.len(), 3);
822 assert_eq!(
823 session_info.restore_disputes[0].initiator,
824 Some(crate::message::DisputeInitiator::Buyer)
825 );
826 assert_eq!(session_info.restore_disputes[1].initiator, None);
827 assert_eq!(
828 session_info.restore_disputes[2].initiator,
829 Some(crate::message::DisputeInitiator::Seller)
830 );
831 } else {
832 panic!("Expected RestoreData payload");
833 }
834 } else {
835 panic!("Expected Restore message");
836 }
837 }
838
839 #[test]
840 fn test_restore_session_message_validation() {
841 let restore_request_message = Message::Restore(MessageKind::new(
843 None,
844 None,
845 None,
846 Action::RestoreSession,
847 None, ));
849
850 assert!(restore_request_message.verify());
852
853 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
855 let wrong_message = Message::Restore(MessageKind::new(
856 None,
857 None,
858 None,
859 Action::RestoreSession,
860 Some(wrong_payload),
861 ));
862
863 assert!(!wrong_message.verify());
865
866 let with_id = Message::Restore(MessageKind::new(
868 Some(uuid!("00000000-0000-0000-0000-000000000001")),
869 None,
870 None,
871 Action::RestoreSession,
872 None,
873 ));
874 assert!(with_id.verify());
875
876 let with_request_id = Message::Restore(MessageKind::new(
877 None,
878 Some(42),
879 None,
880 Action::RestoreSession,
881 None,
882 ));
883 assert!(with_request_id.verify());
884
885 let with_trade_index = Message::Restore(MessageKind::new(
886 None,
887 None,
888 Some(7),
889 Action::RestoreSession,
890 None,
891 ));
892 assert!(with_trade_index.verify());
893 }
894
895 #[test]
896 fn test_restore_session_message_constructor() {
897 let restore_request_message = Message::new_restore(None);
899
900 assert!(matches!(restore_request_message, Message::Restore(_)));
901 assert!(restore_request_message.verify());
902 assert_eq!(
903 restore_request_message.inner_action(),
904 Some(Action::RestoreSession)
905 );
906
907 let restore_session_info = crate::message::RestoreSessionInfo {
909 restore_orders: vec![],
910 restore_disputes: vec![],
911 };
912 let restore_data_message =
913 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
914
915 assert!(matches!(restore_data_message, Message::Restore(_)));
916 assert!(!restore_data_message.verify());
917 }
918
919 #[test]
920 fn test_last_trade_index_valid_message() {
921 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
922 let msg = Message::Restore(kind);
923
924 assert!(msg.verify());
925
926 let json = msg.as_json().unwrap();
928 let decoded = Message::from_json(&json).unwrap();
929 assert!(decoded.verify());
930
931 let inner = decoded.get_inner_message_kind();
933 assert_eq!(inner.trade_index(), 7);
934 assert_eq!(inner.has_trade_index(), (true, 7));
935 }
936
937 #[test]
938 fn test_last_trade_index_without_id_is_valid() {
939 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
941 let msg = Message::Restore(kind);
942 assert!(msg.verify());
943 }
944
945 #[test]
946 fn test_last_trade_index_with_payload_fails_validation() {
947 let kind = MessageKind::new(
949 None,
950 None,
951 Some(3),
952 Action::LastTradeIndex,
953 Some(Payload::TextMessage("ignored".to_string())),
954 );
955 let msg = Message::Restore(kind);
956 assert!(!msg.verify());
957 }
958
959 #[test]
960 fn test_restored_dispute_helper_serialization_roundtrip() {
961 use crate::message::RestoredDisputeHelper;
962
963 let helper = RestoredDisputeHelper {
964 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
965 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
966 dispute_status: "initiated".to_string(),
967 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
968 master_seller_pubkey: Some("npub1sellerkey".to_string()),
969 trade_index_buyer: Some(1),
970 trade_index_seller: Some(2),
971 buyer_dispute: true,
972 seller_dispute: false,
973 };
974
975 let json = serde_json::to_string(&helper).unwrap();
976 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
977
978 assert_eq!(deserialized.dispute_id, helper.dispute_id);
979 assert_eq!(deserialized.order_id, helper.order_id);
980 assert_eq!(deserialized.dispute_status, helper.dispute_status);
981 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
982 assert_eq!(
983 deserialized.master_seller_pubkey,
984 helper.master_seller_pubkey
985 );
986 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
987 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
988 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
989 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
990
991 let helper_seller_dispute = RestoredDisputeHelper {
992 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
993 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
994 dispute_status: "in-progress".to_string(),
995 master_buyer_pubkey: None,
996 master_seller_pubkey: None,
997 trade_index_buyer: None,
998 trade_index_seller: None,
999 buyer_dispute: false,
1000 seller_dispute: true,
1001 };
1002
1003 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
1004 let deserialized_seller: RestoredDisputeHelper =
1005 serde_json::from_str(&json_seller).unwrap();
1006
1007 assert_eq!(
1008 deserialized_seller.dispute_id,
1009 helper_seller_dispute.dispute_id
1010 );
1011 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
1012 assert_eq!(
1013 deserialized_seller.dispute_status,
1014 helper_seller_dispute.dispute_status
1015 );
1016 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
1017 assert_eq!(deserialized_seller.master_seller_pubkey, None);
1018 assert_eq!(deserialized_seller.trade_index_buyer, None);
1019 assert_eq!(deserialized_seller.trade_index_seller, None);
1020 assert!(!deserialized_seller.buyer_dispute);
1021 assert!(deserialized_seller.seller_dispute);
1022 }
1023}