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 pub solver_pubkey: Option<String>,
286}
287
288#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
290#[derive(Debug, Deserialize, Serialize, Clone)]
291pub struct RestoredOrdersInfo {
292 pub order_id: Uuid,
294 pub trade_index: i64,
296 pub status: String,
298}
299
300#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
302#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
303#[serde(rename_all = "lowercase")]
304#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
305pub enum DisputeInitiator {
306 Buyer,
307 Seller,
308}
309
310#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
312#[derive(Debug, Deserialize, Serialize, Clone)]
313pub struct RestoredDisputesInfo {
314 pub dispute_id: Uuid,
316 pub order_id: Uuid,
318 pub trade_index: i64,
320 pub status: String,
322 pub initiator: Option<DisputeInitiator>,
324 pub solver_pubkey: Option<String>,
326}
327
328#[derive(Debug, Deserialize, Serialize, Clone, Default)]
330pub struct RestoreSessionInfo {
331 #[serde(rename = "orders")]
333 pub restore_orders: Vec<RestoredOrdersInfo>,
334 #[serde(rename = "disputes")]
336 pub restore_disputes: Vec<RestoredDisputesInfo>,
337}
338
339#[derive(Debug, Deserialize, Serialize, Clone)]
341#[serde(rename_all = "snake_case")]
342pub enum Payload {
343 Order(SmallOrder),
345 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
347 TextMessage(String),
349 Peer(Peer),
351 RatingUser(u8),
353 Amount(Amount),
355 Dispute(Uuid, Option<SolverDisputeInfo>),
357 CantDo(Option<CantDoReason>),
359 NextTrade(String, u32),
363 PaymentFailed(PaymentFailedInfo),
365 RestoreData(RestoreSessionInfo),
367 Ids(Vec<Uuid>),
369 Orders(Vec<SmallOrder>),
371}
372
373#[allow(dead_code)]
374impl MessageKind {
375 pub fn new(
377 id: Option<Uuid>,
378 request_id: Option<u64>,
379 trade_index: Option<i64>,
380 action: Action,
381 payload: Option<Payload>,
382 ) -> Self {
383 Self {
384 version: PROTOCOL_VER,
385 request_id,
386 trade_index,
387 id,
388 action,
389 payload,
390 }
391 }
392 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
394 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
395 }
396 pub fn as_json(&self) -> Result<String, ServiceError> {
398 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
399 }
400
401 pub fn get_action(&self) -> Action {
403 self.action.clone()
404 }
405
406 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
408 match &self.payload {
409 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
410 None => Ok(None),
411 _ => Err(ServiceError::InvalidPayload),
412 }
413 }
414
415 pub fn get_rating(&self) -> Result<u8, ServiceError> {
416 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
417 if !(MIN_RATING..=MAX_RATING).contains(&v) {
418 return Err(ServiceError::InvalidRatingValue);
419 }
420 Ok(v)
421 } else {
422 Err(ServiceError::InvalidRating)
423 }
424 }
425
426 pub fn verify(&self) -> bool {
428 match &self.action {
429 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
430 Action::PayInvoice | Action::AddInvoice => {
431 if self.id.is_none() {
432 return false;
433 }
434 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
435 }
436 Action::TakeSell
437 | Action::TakeBuy
438 | Action::FiatSent
439 | Action::FiatSentOk
440 | Action::Release
441 | Action::Released
442 | Action::Dispute
443 | Action::AdminCancel
444 | Action::AdminCanceled
445 | Action::AdminSettle
446 | Action::AdminSettled
447 | Action::Rate
448 | Action::RateReceived
449 | Action::AdminTakeDispute
450 | Action::AdminTookDispute
451 | Action::DisputeInitiatedByYou
452 | Action::DisputeInitiatedByPeer
453 | Action::WaitingBuyerInvoice
454 | Action::PurchaseCompleted
455 | Action::HoldInvoicePaymentAccepted
456 | Action::HoldInvoicePaymentSettled
457 | Action::HoldInvoicePaymentCanceled
458 | Action::WaitingSellerToPay
459 | Action::BuyerTookOrder
460 | Action::BuyerInvoiceAccepted
461 | Action::CooperativeCancelInitiatedByYou
462 | Action::CooperativeCancelInitiatedByPeer
463 | Action::CooperativeCancelAccepted
464 | Action::Cancel
465 | Action::InvoiceUpdated
466 | Action::AdminAddSolver
467 | Action::SendDm
468 | Action::TradePubkey
469 | Action::Canceled => {
470 if self.id.is_none() {
471 return false;
472 }
473 true
474 }
475 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
476 Action::PaymentFailed => {
477 if self.id.is_none() {
478 return false;
479 }
480 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
481 }
482 Action::RateUser => {
483 matches!(&self.payload, Some(Payload::RatingUser(_)))
484 }
485 Action::CantDo => {
486 matches!(&self.payload, Some(Payload::CantDo(_)))
487 }
488 Action::Orders => {
489 matches!(
490 &self.payload,
491 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
492 )
493 }
494 }
495 }
496
497 pub fn get_order(&self) -> Option<&SmallOrder> {
498 if self.action != Action::NewOrder {
499 return None;
500 }
501 match &self.payload {
502 Some(Payload::Order(o)) => Some(o),
503 _ => None,
504 }
505 }
506
507 pub fn get_payment_request(&self) -> Option<String> {
508 if self.action != Action::TakeSell
509 && self.action != Action::AddInvoice
510 && self.action != Action::NewOrder
511 {
512 return None;
513 }
514 match &self.payload {
515 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
516 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
517 _ => None,
518 }
519 }
520
521 pub fn get_amount(&self) -> Option<Amount> {
522 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
523 return None;
524 }
525 match &self.payload {
526 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
527 Some(Payload::Amount(amount)) => Some(*amount),
528 _ => None,
529 }
530 }
531
532 pub fn get_payload(&self) -> Option<&Payload> {
533 self.payload.as_ref()
534 }
535
536 pub fn has_trade_index(&self) -> (bool, i64) {
537 if let Some(index) = self.trade_index {
538 return (true, index);
539 }
540 (false, 0)
541 }
542
543 pub fn trade_index(&self) -> i64 {
544 if let Some(index) = self.trade_index {
545 return index;
546 }
547 0
548 }
549}
550
551#[cfg(test)]
552mod test {
553 use crate::message::{Action, Message, MessageKind, Payload, Peer};
554 use crate::user::UserInfo;
555 use nostr_sdk::Keys;
556 use uuid::uuid;
557
558 #[test]
559 fn test_peer_with_reputation() {
560 let reputation = UserInfo {
562 rating: 4.5,
563 reviews: 10,
564 operating_days: 30,
565 };
566 let peer = Peer::new(
567 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
568 Some(reputation.clone()),
569 );
570
571 assert_eq!(
573 peer.pubkey,
574 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
575 );
576 assert!(peer.reputation.is_some());
577 let peer_reputation = peer.reputation.clone().unwrap();
578 assert_eq!(peer_reputation.rating, 4.5);
579 assert_eq!(peer_reputation.reviews, 10);
580 assert_eq!(peer_reputation.operating_days, 30);
581
582 let json = peer.as_json().unwrap();
584 let deserialized_peer = Peer::from_json(&json).unwrap();
585 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
586 assert!(deserialized_peer.reputation.is_some());
587 let deserialized_reputation = deserialized_peer.reputation.unwrap();
588 assert_eq!(deserialized_reputation.rating, 4.5);
589 assert_eq!(deserialized_reputation.reviews, 10);
590 assert_eq!(deserialized_reputation.operating_days, 30);
591 }
592
593 #[test]
594 fn test_peer_without_reputation() {
595 let peer = Peer::new(
597 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
598 None,
599 );
600
601 assert_eq!(
603 peer.pubkey,
604 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
605 );
606 assert!(peer.reputation.is_none());
607
608 let json = peer.as_json().unwrap();
610 let deserialized_peer = Peer::from_json(&json).unwrap();
611 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
612 assert!(deserialized_peer.reputation.is_none());
613 }
614
615 #[test]
616 fn test_peer_in_message() {
617 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
618
619 let reputation = UserInfo {
621 rating: 4.5,
622 reviews: 10,
623 operating_days: 30,
624 };
625 let peer_with_reputation = Peer::new(
626 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
627 Some(reputation),
628 );
629 let payload_with_reputation = Payload::Peer(peer_with_reputation);
630 let message_with_reputation = Message::Order(MessageKind::new(
631 Some(uuid),
632 Some(1),
633 Some(2),
634 Action::FiatSentOk,
635 Some(payload_with_reputation),
636 ));
637
638 assert!(message_with_reputation.verify());
640 let message_json = message_with_reputation.as_json().unwrap();
641 let deserialized_message = Message::from_json(&message_json).unwrap();
642 assert!(deserialized_message.verify());
643
644 let peer_without_reputation = Peer::new(
646 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
647 None,
648 );
649 let payload_without_reputation = Payload::Peer(peer_without_reputation);
650 let message_without_reputation = Message::Order(MessageKind::new(
651 Some(uuid),
652 Some(1),
653 Some(2),
654 Action::FiatSentOk,
655 Some(payload_without_reputation),
656 ));
657
658 assert!(message_without_reputation.verify());
660 let message_json = message_without_reputation.as_json().unwrap();
661 let deserialized_message = Message::from_json(&message_json).unwrap();
662 assert!(deserialized_message.verify());
663 }
664
665 #[test]
666 fn test_payment_failed_payload() {
667 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
668
669 let payment_failed_info = crate::message::PaymentFailedInfo {
671 payment_attempts: 3,
672 payment_retries_interval: 60,
673 };
674
675 let payload = Payload::PaymentFailed(payment_failed_info);
676 let message = Message::Order(MessageKind::new(
677 Some(uuid),
678 Some(1),
679 Some(2),
680 Action::PaymentFailed,
681 Some(payload),
682 ));
683
684 assert!(message.verify());
686
687 let message_json = message.as_json().unwrap();
689
690 let deserialized_message = Message::from_json(&message_json).unwrap();
692 assert!(deserialized_message.verify());
693
694 if let Message::Order(kind) = deserialized_message {
696 if let Some(Payload::PaymentFailed(info)) = kind.payload {
697 assert_eq!(info.payment_attempts, 3);
698 assert_eq!(info.payment_retries_interval, 60);
699 } else {
700 panic!("Expected PaymentFailed payload");
701 }
702 } else {
703 panic!("Expected Order message");
704 }
705 }
706
707 #[test]
708 fn test_message_payload_signature() {
709 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
710 let peer = Peer::new(
711 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
712 None, );
714 let payload = Payload::Peer(peer);
715 let test_message = Message::Order(MessageKind::new(
716 Some(uuid),
717 Some(1),
718 Some(2),
719 Action::FiatSentOk,
720 Some(payload),
721 ));
722 assert!(test_message.verify());
723 let test_message_json = test_message.as_json().unwrap();
724 let trade_keys =
726 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
727 .unwrap();
728 let sig = Message::sign(test_message_json.clone(), &trade_keys);
729
730 assert!(Message::verify_signature(
731 test_message_json,
732 trade_keys.public_key(),
733 sig
734 ));
735 }
736
737 #[test]
738 fn test_restore_session_message() {
739 let restore_request_message = Message::Restore(MessageKind::new(
741 None,
742 None,
743 None,
744 Action::RestoreSession,
745 None,
746 ));
747
748 assert!(restore_request_message.verify());
750 assert_eq!(
751 restore_request_message.inner_action(),
752 Some(Action::RestoreSession)
753 );
754
755 let message_json = restore_request_message.as_json().unwrap();
757 let deserialized_message = Message::from_json(&message_json).unwrap();
758 assert!(deserialized_message.verify());
759 assert_eq!(
760 deserialized_message.inner_action(),
761 Some(Action::RestoreSession)
762 );
763
764 let restored_orders = vec![
766 crate::message::RestoredOrdersInfo {
767 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
768 trade_index: 1,
769 status: "active".to_string(),
770 },
771 crate::message::RestoredOrdersInfo {
772 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
773 trade_index: 2,
774 status: "success".to_string(),
775 },
776 ];
777
778 let restored_disputes = vec![
779 crate::message::RestoredDisputesInfo {
780 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
781 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
782 trade_index: 1,
783 status: "initiated".to_string(),
784 initiator: Some(crate::message::DisputeInitiator::Buyer),
785 solver_pubkey: None,
786 },
787 crate::message::RestoredDisputesInfo {
788 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
789 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
790 trade_index: 2,
791 status: "in-progress".to_string(),
792 initiator: None,
793 solver_pubkey: Some(
794 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
795 ),
796 },
797 crate::message::RestoredDisputesInfo {
798 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
799 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
800 trade_index: 3,
801 status: "initiated".to_string(),
802 initiator: Some(crate::message::DisputeInitiator::Seller),
803 solver_pubkey: None,
804 },
805 ];
806
807 let restore_session_info = crate::message::RestoreSessionInfo {
808 restore_orders: restored_orders.clone(),
809 restore_disputes: restored_disputes.clone(),
810 };
811
812 let restore_data_payload = Payload::RestoreData(restore_session_info);
813 let restore_data_message = Message::Restore(MessageKind::new(
814 None,
815 None,
816 None,
817 Action::RestoreSession,
818 Some(restore_data_payload),
819 ));
820
821 assert!(!restore_data_message.verify());
823
824 let message_json = restore_data_message.as_json().unwrap();
826 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
827
828 if let Message::Restore(kind) = deserialized_restore_message {
829 if let Some(Payload::RestoreData(session_info)) = kind.payload {
830 assert_eq!(session_info.restore_disputes.len(), 3);
831 assert_eq!(
832 session_info.restore_disputes[0].initiator,
833 Some(crate::message::DisputeInitiator::Buyer)
834 );
835 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
836 assert_eq!(session_info.restore_disputes[1].initiator, None);
837 assert_eq!(
838 session_info.restore_disputes[1].solver_pubkey,
839 Some(
840 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
841 .to_string()
842 )
843 );
844 assert_eq!(
845 session_info.restore_disputes[2].initiator,
846 Some(crate::message::DisputeInitiator::Seller)
847 );
848 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
849 } else {
850 panic!("Expected RestoreData payload");
851 }
852 } else {
853 panic!("Expected Restore message");
854 }
855 }
856
857 #[test]
858 fn test_restore_session_message_validation() {
859 let restore_request_message = Message::Restore(MessageKind::new(
861 None,
862 None,
863 None,
864 Action::RestoreSession,
865 None, ));
867
868 assert!(restore_request_message.verify());
870
871 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
873 let wrong_message = Message::Restore(MessageKind::new(
874 None,
875 None,
876 None,
877 Action::RestoreSession,
878 Some(wrong_payload),
879 ));
880
881 assert!(!wrong_message.verify());
883
884 let with_id = Message::Restore(MessageKind::new(
886 Some(uuid!("00000000-0000-0000-0000-000000000001")),
887 None,
888 None,
889 Action::RestoreSession,
890 None,
891 ));
892 assert!(with_id.verify());
893
894 let with_request_id = Message::Restore(MessageKind::new(
895 None,
896 Some(42),
897 None,
898 Action::RestoreSession,
899 None,
900 ));
901 assert!(with_request_id.verify());
902
903 let with_trade_index = Message::Restore(MessageKind::new(
904 None,
905 None,
906 Some(7),
907 Action::RestoreSession,
908 None,
909 ));
910 assert!(with_trade_index.verify());
911 }
912
913 #[test]
914 fn test_restore_session_message_constructor() {
915 let restore_request_message = Message::new_restore(None);
917
918 assert!(matches!(restore_request_message, Message::Restore(_)));
919 assert!(restore_request_message.verify());
920 assert_eq!(
921 restore_request_message.inner_action(),
922 Some(Action::RestoreSession)
923 );
924
925 let restore_session_info = crate::message::RestoreSessionInfo {
927 restore_orders: vec![],
928 restore_disputes: vec![],
929 };
930 let restore_data_message =
931 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
932
933 assert!(matches!(restore_data_message, Message::Restore(_)));
934 assert!(!restore_data_message.verify());
935 }
936
937 #[test]
938 fn test_last_trade_index_valid_message() {
939 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
940 let msg = Message::Restore(kind);
941
942 assert!(msg.verify());
943
944 let json = msg.as_json().unwrap();
946 let decoded = Message::from_json(&json).unwrap();
947 assert!(decoded.verify());
948
949 let inner = decoded.get_inner_message_kind();
951 assert_eq!(inner.trade_index(), 7);
952 assert_eq!(inner.has_trade_index(), (true, 7));
953 }
954
955 #[test]
956 fn test_last_trade_index_without_id_is_valid() {
957 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
959 let msg = Message::Restore(kind);
960 assert!(msg.verify());
961 }
962
963 #[test]
964 fn test_last_trade_index_with_payload_fails_validation() {
965 let kind = MessageKind::new(
967 None,
968 None,
969 Some(3),
970 Action::LastTradeIndex,
971 Some(Payload::TextMessage("ignored".to_string())),
972 );
973 let msg = Message::Restore(kind);
974 assert!(!msg.verify());
975 }
976
977 #[test]
978 fn test_restored_dispute_helper_serialization_roundtrip() {
979 use crate::message::RestoredDisputeHelper;
980
981 let helper = RestoredDisputeHelper {
982 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
983 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
984 dispute_status: "initiated".to_string(),
985 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
986 master_seller_pubkey: Some("npub1sellerkey".to_string()),
987 trade_index_buyer: Some(1),
988 trade_index_seller: Some(2),
989 buyer_dispute: true,
990 seller_dispute: false,
991 solver_pubkey: None,
992 };
993
994 let json = serde_json::to_string(&helper).unwrap();
995 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
996
997 assert_eq!(deserialized.dispute_id, helper.dispute_id);
998 assert_eq!(deserialized.order_id, helper.order_id);
999 assert_eq!(deserialized.dispute_status, helper.dispute_status);
1000 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
1001 assert_eq!(
1002 deserialized.master_seller_pubkey,
1003 helper.master_seller_pubkey
1004 );
1005 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
1006 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
1007 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
1008 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
1009 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
1010
1011 let helper_seller_dispute = RestoredDisputeHelper {
1012 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1013 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1014 dispute_status: "in-progress".to_string(),
1015 master_buyer_pubkey: None,
1016 master_seller_pubkey: None,
1017 trade_index_buyer: None,
1018 trade_index_seller: None,
1019 buyer_dispute: false,
1020 seller_dispute: true,
1021 solver_pubkey: Some(
1022 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1023 ),
1024 };
1025
1026 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
1027 let deserialized_seller: RestoredDisputeHelper =
1028 serde_json::from_str(&json_seller).unwrap();
1029
1030 assert_eq!(
1031 deserialized_seller.dispute_id,
1032 helper_seller_dispute.dispute_id
1033 );
1034 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
1035 assert_eq!(
1036 deserialized_seller.dispute_status,
1037 helper_seller_dispute.dispute_status
1038 );
1039 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
1040 assert_eq!(deserialized_seller.master_seller_pubkey, None);
1041 assert_eq!(deserialized_seller.trade_index_buyer, None);
1042 assert_eq!(deserialized_seller.trade_index_seller, None);
1043 assert!(!deserialized_seller.buyer_dispute);
1044 assert!(deserialized_seller.seller_dispute);
1045 assert_eq!(
1046 deserialized_seller.solver_pubkey,
1047 helper_seller_dispute.solver_pubkey
1048 );
1049 }
1050}