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}
280
281#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
283#[derive(Debug, Deserialize, Serialize, Clone)]
284pub struct RestoredOrdersInfo {
285 pub order_id: Uuid,
287 pub trade_index: i64,
289 pub status: String,
291}
292
293#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
295#[derive(Debug, Deserialize, Serialize, Clone)]
296pub struct RestoredDisputesInfo {
297 pub dispute_id: Uuid,
299 pub order_id: Uuid,
301 pub trade_index: i64,
303 pub status: String,
305}
306
307#[derive(Debug, Deserialize, Serialize, Clone, Default)]
309pub struct RestoreSessionInfo {
310 #[serde(rename = "orders")]
312 pub restore_orders: Vec<RestoredOrdersInfo>,
313 #[serde(rename = "disputes")]
315 pub restore_disputes: Vec<RestoredDisputesInfo>,
316}
317
318#[derive(Debug, Deserialize, Serialize, Clone)]
320#[serde(rename_all = "snake_case")]
321pub enum Payload {
322 Order(SmallOrder),
324 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
326 TextMessage(String),
328 Peer(Peer),
330 RatingUser(u8),
332 Amount(Amount),
334 Dispute(Uuid, Option<SolverDisputeInfo>),
336 CantDo(Option<CantDoReason>),
338 NextTrade(String, u32),
342 PaymentFailed(PaymentFailedInfo),
344 RestoreData(RestoreSessionInfo),
346 Ids(Vec<Uuid>),
348 Orders(Vec<SmallOrder>),
350}
351
352#[allow(dead_code)]
353impl MessageKind {
354 pub fn new(
356 id: Option<Uuid>,
357 request_id: Option<u64>,
358 trade_index: Option<i64>,
359 action: Action,
360 payload: Option<Payload>,
361 ) -> Self {
362 Self {
363 version: PROTOCOL_VER,
364 request_id,
365 trade_index,
366 id,
367 action,
368 payload,
369 }
370 }
371 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
373 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
374 }
375 pub fn as_json(&self) -> Result<String, ServiceError> {
377 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
378 }
379
380 pub fn get_action(&self) -> Action {
382 self.action.clone()
383 }
384
385 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
387 match &self.payload {
388 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
389 None => Ok(None),
390 _ => Err(ServiceError::InvalidPayload),
391 }
392 }
393
394 pub fn get_rating(&self) -> Result<u8, ServiceError> {
395 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
396 if !(MIN_RATING..=MAX_RATING).contains(&v) {
397 return Err(ServiceError::InvalidRatingValue);
398 }
399 Ok(v)
400 } else {
401 Err(ServiceError::InvalidRating)
402 }
403 }
404
405 pub fn verify(&self) -> bool {
407 match &self.action {
408 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
409 Action::PayInvoice | Action::AddInvoice => {
410 if self.id.is_none() {
411 return false;
412 }
413 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
414 }
415 Action::TakeSell
416 | Action::TakeBuy
417 | Action::FiatSent
418 | Action::FiatSentOk
419 | Action::Release
420 | Action::Released
421 | Action::Dispute
422 | Action::AdminCancel
423 | Action::AdminCanceled
424 | Action::AdminSettle
425 | Action::AdminSettled
426 | Action::Rate
427 | Action::RateReceived
428 | Action::AdminTakeDispute
429 | Action::AdminTookDispute
430 | Action::DisputeInitiatedByYou
431 | Action::DisputeInitiatedByPeer
432 | Action::WaitingBuyerInvoice
433 | Action::PurchaseCompleted
434 | Action::HoldInvoicePaymentAccepted
435 | Action::HoldInvoicePaymentSettled
436 | Action::HoldInvoicePaymentCanceled
437 | Action::WaitingSellerToPay
438 | Action::BuyerTookOrder
439 | Action::BuyerInvoiceAccepted
440 | Action::CooperativeCancelInitiatedByYou
441 | Action::CooperativeCancelInitiatedByPeer
442 | Action::CooperativeCancelAccepted
443 | Action::Cancel
444 | Action::InvoiceUpdated
445 | Action::AdminAddSolver
446 | Action::SendDm
447 | Action::TradePubkey
448 | Action::Canceled => {
449 if self.id.is_none() {
450 return false;
451 }
452 true
453 }
454 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
455 Action::PaymentFailed => {
456 if self.id.is_none() {
457 return false;
458 }
459 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
460 }
461 Action::RateUser => {
462 matches!(&self.payload, Some(Payload::RatingUser(_)))
463 }
464 Action::CantDo => {
465 matches!(&self.payload, Some(Payload::CantDo(_)))
466 }
467 Action::Orders => {
468 matches!(
469 &self.payload,
470 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
471 )
472 }
473 }
474 }
475
476 pub fn get_order(&self) -> Option<&SmallOrder> {
477 if self.action != Action::NewOrder {
478 return None;
479 }
480 match &self.payload {
481 Some(Payload::Order(o)) => Some(o),
482 _ => None,
483 }
484 }
485
486 pub fn get_payment_request(&self) -> Option<String> {
487 if self.action != Action::TakeSell
488 && self.action != Action::AddInvoice
489 && self.action != Action::NewOrder
490 {
491 return None;
492 }
493 match &self.payload {
494 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
495 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
496 _ => None,
497 }
498 }
499
500 pub fn get_amount(&self) -> Option<Amount> {
501 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
502 return None;
503 }
504 match &self.payload {
505 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
506 Some(Payload::Amount(amount)) => Some(*amount),
507 _ => None,
508 }
509 }
510
511 pub fn get_payload(&self) -> Option<&Payload> {
512 self.payload.as_ref()
513 }
514
515 pub fn has_trade_index(&self) -> (bool, i64) {
516 if let Some(index) = self.trade_index {
517 return (true, index);
518 }
519 (false, 0)
520 }
521
522 pub fn trade_index(&self) -> i64 {
523 if let Some(index) = self.trade_index {
524 return index;
525 }
526 0
527 }
528}
529
530#[cfg(test)]
531mod test {
532 use crate::message::{Action, Message, MessageKind, Payload, Peer};
533 use crate::user::UserInfo;
534 use nostr_sdk::Keys;
535 use uuid::uuid;
536
537 #[test]
538 fn test_peer_with_reputation() {
539 let reputation = UserInfo {
541 rating: 4.5,
542 reviews: 10,
543 operating_days: 30,
544 };
545 let peer = Peer::new(
546 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
547 Some(reputation.clone()),
548 );
549
550 assert_eq!(
552 peer.pubkey,
553 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
554 );
555 assert!(peer.reputation.is_some());
556 let peer_reputation = peer.reputation.clone().unwrap();
557 assert_eq!(peer_reputation.rating, 4.5);
558 assert_eq!(peer_reputation.reviews, 10);
559 assert_eq!(peer_reputation.operating_days, 30);
560
561 let json = peer.as_json().unwrap();
563 let deserialized_peer = Peer::from_json(&json).unwrap();
564 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
565 assert!(deserialized_peer.reputation.is_some());
566 let deserialized_reputation = deserialized_peer.reputation.unwrap();
567 assert_eq!(deserialized_reputation.rating, 4.5);
568 assert_eq!(deserialized_reputation.reviews, 10);
569 assert_eq!(deserialized_reputation.operating_days, 30);
570 }
571
572 #[test]
573 fn test_peer_without_reputation() {
574 let peer = Peer::new(
576 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
577 None,
578 );
579
580 assert_eq!(
582 peer.pubkey,
583 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
584 );
585 assert!(peer.reputation.is_none());
586
587 let json = peer.as_json().unwrap();
589 let deserialized_peer = Peer::from_json(&json).unwrap();
590 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
591 assert!(deserialized_peer.reputation.is_none());
592 }
593
594 #[test]
595 fn test_peer_in_message() {
596 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
597
598 let reputation = UserInfo {
600 rating: 4.5,
601 reviews: 10,
602 operating_days: 30,
603 };
604 let peer_with_reputation = Peer::new(
605 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
606 Some(reputation),
607 );
608 let payload_with_reputation = Payload::Peer(peer_with_reputation);
609 let message_with_reputation = Message::Order(MessageKind::new(
610 Some(uuid),
611 Some(1),
612 Some(2),
613 Action::FiatSentOk,
614 Some(payload_with_reputation),
615 ));
616
617 assert!(message_with_reputation.verify());
619 let message_json = message_with_reputation.as_json().unwrap();
620 let deserialized_message = Message::from_json(&message_json).unwrap();
621 assert!(deserialized_message.verify());
622
623 let peer_without_reputation = Peer::new(
625 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
626 None,
627 );
628 let payload_without_reputation = Payload::Peer(peer_without_reputation);
629 let message_without_reputation = Message::Order(MessageKind::new(
630 Some(uuid),
631 Some(1),
632 Some(2),
633 Action::FiatSentOk,
634 Some(payload_without_reputation),
635 ));
636
637 assert!(message_without_reputation.verify());
639 let message_json = message_without_reputation.as_json().unwrap();
640 let deserialized_message = Message::from_json(&message_json).unwrap();
641 assert!(deserialized_message.verify());
642 }
643
644 #[test]
645 fn test_payment_failed_payload() {
646 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
647
648 let payment_failed_info = crate::message::PaymentFailedInfo {
650 payment_attempts: 3,
651 payment_retries_interval: 60,
652 };
653
654 let payload = Payload::PaymentFailed(payment_failed_info);
655 let message = Message::Order(MessageKind::new(
656 Some(uuid),
657 Some(1),
658 Some(2),
659 Action::PaymentFailed,
660 Some(payload),
661 ));
662
663 assert!(message.verify());
665
666 let message_json = message.as_json().unwrap();
668
669 let deserialized_message = Message::from_json(&message_json).unwrap();
671 assert!(deserialized_message.verify());
672
673 if let Message::Order(kind) = deserialized_message {
675 if let Some(Payload::PaymentFailed(info)) = kind.payload {
676 assert_eq!(info.payment_attempts, 3);
677 assert_eq!(info.payment_retries_interval, 60);
678 } else {
679 panic!("Expected PaymentFailed payload");
680 }
681 } else {
682 panic!("Expected Order message");
683 }
684 }
685
686 #[test]
687 fn test_message_payload_signature() {
688 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
689 let peer = Peer::new(
690 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
691 None, );
693 let payload = Payload::Peer(peer);
694 let test_message = Message::Order(MessageKind::new(
695 Some(uuid),
696 Some(1),
697 Some(2),
698 Action::FiatSentOk,
699 Some(payload),
700 ));
701 assert!(test_message.verify());
702 let test_message_json = test_message.as_json().unwrap();
703 let trade_keys =
705 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
706 .unwrap();
707 let sig = Message::sign(test_message_json.clone(), &trade_keys);
708
709 assert!(Message::verify_signature(
710 test_message_json,
711 trade_keys.public_key(),
712 sig
713 ));
714 }
715
716 #[test]
717 fn test_restore_session_message() {
718 let restore_request_message = Message::Restore(MessageKind::new(
720 None,
721 None,
722 None,
723 Action::RestoreSession,
724 None,
725 ));
726
727 assert!(restore_request_message.verify());
729 assert_eq!(
730 restore_request_message.inner_action(),
731 Some(Action::RestoreSession)
732 );
733
734 let message_json = restore_request_message.as_json().unwrap();
736 let deserialized_message = Message::from_json(&message_json).unwrap();
737 assert!(deserialized_message.verify());
738 assert_eq!(
739 deserialized_message.inner_action(),
740 Some(Action::RestoreSession)
741 );
742
743 let restored_orders = vec![
745 crate::message::RestoredOrdersInfo {
746 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
747 trade_index: 1,
748 status: "active".to_string(),
749 },
750 crate::message::RestoredOrdersInfo {
751 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
752 trade_index: 2,
753 status: "success".to_string(),
754 },
755 ];
756
757 let restored_disputes = vec![crate::message::RestoredDisputesInfo {
758 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
759 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
760 trade_index: 1,
761 status: "initiated".to_string(),
762 }];
763
764 let restore_session_info = crate::message::RestoreSessionInfo {
765 restore_orders: restored_orders.clone(),
766 restore_disputes: restored_disputes.clone(),
767 };
768
769 let restore_data_payload = Payload::RestoreData(restore_session_info);
770 let restore_data_message = Message::Restore(MessageKind::new(
771 None,
772 None,
773 None,
774 Action::RestoreSession,
775 Some(restore_data_payload),
776 ));
777
778 assert!(!restore_data_message.verify());
780 }
781
782 #[test]
783 fn test_restore_session_message_validation() {
784 let restore_request_message = Message::Restore(MessageKind::new(
786 None,
787 None,
788 None,
789 Action::RestoreSession,
790 None, ));
792
793 assert!(restore_request_message.verify());
795
796 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
798 let wrong_message = Message::Restore(MessageKind::new(
799 None,
800 None,
801 None,
802 Action::RestoreSession,
803 Some(wrong_payload),
804 ));
805
806 assert!(!wrong_message.verify());
808
809 let with_id = Message::Restore(MessageKind::new(
811 Some(uuid!("00000000-0000-0000-0000-000000000001")),
812 None,
813 None,
814 Action::RestoreSession,
815 None,
816 ));
817 assert!(with_id.verify());
818
819 let with_request_id = Message::Restore(MessageKind::new(
820 None,
821 Some(42),
822 None,
823 Action::RestoreSession,
824 None,
825 ));
826 assert!(with_request_id.verify());
827
828 let with_trade_index = Message::Restore(MessageKind::new(
829 None,
830 None,
831 Some(7),
832 Action::RestoreSession,
833 None,
834 ));
835 assert!(with_trade_index.verify());
836 }
837
838 #[test]
839 fn test_restore_session_message_constructor() {
840 let restore_request_message = Message::new_restore(None);
842
843 assert!(matches!(restore_request_message, Message::Restore(_)));
844 assert!(restore_request_message.verify());
845 assert_eq!(
846 restore_request_message.inner_action(),
847 Some(Action::RestoreSession)
848 );
849
850 let restore_session_info = crate::message::RestoreSessionInfo {
852 restore_orders: vec![],
853 restore_disputes: vec![],
854 };
855 let restore_data_message =
856 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
857
858 assert!(matches!(restore_data_message, Message::Restore(_)));
859 assert!(!restore_data_message.verify());
860 }
861
862 #[test]
863 fn test_last_trade_index_valid_message() {
864 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
865 let msg = Message::Restore(kind);
866
867 assert!(msg.verify());
868
869 let json = msg.as_json().unwrap();
871 let decoded = Message::from_json(&json).unwrap();
872 assert!(decoded.verify());
873
874 let inner = decoded.get_inner_message_kind();
876 assert_eq!(inner.trade_index(), 7);
877 assert_eq!(inner.has_trade_index(), (true, 7));
878 }
879
880 #[test]
881 fn test_last_trade_index_without_id_is_valid() {
882 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
884 let msg = Message::Restore(kind);
885 assert!(msg.verify());
886 }
887
888 #[test]
889 fn test_last_trade_index_with_payload_fails_validation() {
890 let kind = MessageKind::new(
892 None,
893 None,
894 Some(3),
895 Action::LastTradeIndex,
896 Some(Payload::TextMessage("ignored".to_string())),
897 );
898 let msg = Message::Restore(kind);
899 assert!(!msg.verify());
900 }
901}