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