1use crate::prelude::*;
14use bitcoin::hashes::sha256::Hash as Sha256Hash;
15use bitcoin::hashes::Hash;
16use bitcoin::key::Secp256k1;
17use bitcoin::secp256k1::Message as BitcoinMessage;
18use nostr_sdk::prelude::*;
19#[cfg(feature = "sqlx")]
20use sqlx::FromRow;
21#[cfg(feature = "sqlx")]
22use sqlx_crud::SqlxCrud;
23
24use std::fmt;
25use uuid::Uuid;
26
27#[derive(Debug, Deserialize, Serialize, Clone)]
34pub struct Peer {
35 pub pubkey: String,
37 pub reputation: Option<UserInfo>,
40}
41
42impl Peer {
43 pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
45 Self { pubkey, reputation }
46 }
47
48 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
50 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
51 }
52
53 pub fn as_json(&self) -> Result<String, ServiceError> {
55 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
56 }
57}
58
59#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
65#[serde(rename_all = "kebab-case")]
66pub enum Action {
67 NewOrder,
69 TakeSell,
72 TakeBuy,
74 PayInvoice,
77 PayBondInvoice,
84 FiatSent,
86 FiatSentOk,
88 Release,
90 Released,
92 Cancel,
94 Canceled,
96 CooperativeCancelInitiatedByYou,
98 CooperativeCancelInitiatedByPeer,
100 DisputeInitiatedByYou,
102 DisputeInitiatedByPeer,
104 CooperativeCancelAccepted,
106 BuyerInvoiceAccepted,
108 PurchaseCompleted,
110 HoldInvoicePaymentAccepted,
112 HoldInvoicePaymentSettled,
114 HoldInvoicePaymentCanceled,
116 WaitingSellerToPay,
118 WaitingBuyerInvoice,
120 AddInvoice,
123 BuyerTookOrder,
125 Rate,
127 RateUser,
129 RateReceived,
131 CantDo,
133 Dispute,
135 AdminCancel,
137 AdminCanceled,
139 AdminSettle,
141 AdminSettled,
143 AdminAddSolver,
145 AdminTakeDispute,
147 AdminTookDispute,
149 PaymentFailed,
152 InvoiceUpdated,
154 SendDm,
156 TradePubkey,
158 RestoreSession,
160 LastTradeIndex,
163 Orders,
166}
167
168impl fmt::Display for Action {
169 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
170 write!(f, "{self:?}")
171 }
172}
173
174#[derive(Debug, Clone, Deserialize, Serialize)]
181#[serde(rename_all = "kebab-case")]
182pub enum Message {
183 Order(MessageKind),
185 Dispute(MessageKind),
187 CantDo(MessageKind),
189 Rate(MessageKind),
191 Dm(MessageKind),
193 Restore(MessageKind),
195}
196
197impl Message {
198 pub fn new_order(
201 id: Option<Uuid>,
202 request_id: Option<u64>,
203 trade_index: Option<i64>,
204 action: Action,
205 payload: Option<Payload>,
206 ) -> Self {
207 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
208 Self::Order(kind)
209 }
210
211 pub fn new_dispute(
214 id: Option<Uuid>,
215 request_id: Option<u64>,
216 trade_index: Option<i64>,
217 action: Action,
218 payload: Option<Payload>,
219 ) -> Self {
220 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
221
222 Self::Dispute(kind)
223 }
224
225 pub fn new_restore(payload: Option<Payload>) -> Self {
230 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
231 Self::Restore(kind)
232 }
233
234 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
237 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
238
239 Self::CantDo(kind)
240 }
241
242 pub fn new_dm(
244 id: Option<Uuid>,
245 request_id: Option<u64>,
246 action: Action,
247 payload: Option<Payload>,
248 ) -> Self {
249 let kind = MessageKind::new(id, request_id, None, action, payload);
250
251 Self::Dm(kind)
252 }
253
254 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
256 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
257 }
258
259 pub fn as_json(&self) -> Result<String, ServiceError> {
261 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
262 }
263
264 pub fn get_inner_message_kind(&self) -> &MessageKind {
266 match self {
267 Message::Dispute(k)
268 | Message::Order(k)
269 | Message::CantDo(k)
270 | Message::Rate(k)
271 | Message::Dm(k)
272 | Message::Restore(k) => k,
273 }
274 }
275
276 pub fn inner_action(&self) -> Option<Action> {
281 match self {
282 Message::Dispute(a)
283 | Message::Order(a)
284 | Message::CantDo(a)
285 | Message::Rate(a)
286 | Message::Dm(a)
287 | Message::Restore(a) => Some(a.get_action()),
288 }
289 }
290
291 pub fn verify(&self) -> bool {
294 match self {
295 Message::Order(m)
296 | Message::Dispute(m)
297 | Message::CantDo(m)
298 | Message::Rate(m)
299 | Message::Dm(m)
300 | Message::Restore(m) => m.verify(),
301 }
302 }
303
304 pub fn sign(message: String, keys: &Keys) -> Signature {
313 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
314 let hash = hash.to_byte_array();
315 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
316
317 keys.sign_schnorr(&message)
318 }
319
320 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
326 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
328 let hash = hash.to_byte_array();
329 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
330
331 let secp = Secp256k1::verification_only();
333 if let Ok(xonlykey) = pubkey.xonly() {
335 xonlykey.verify(&secp, &message, &sig).is_ok()
336 } else {
337 false
338 }
339 }
340}
341
342#[derive(Debug, Clone, Deserialize, Serialize)]
349pub struct MessageKind {
350 pub version: u8,
353 pub request_id: Option<u64>,
356 pub trade_index: Option<i64>,
359 #[serde(skip_serializing_if = "Option::is_none")]
362 pub id: Option<Uuid>,
363 pub action: Action,
365 pub payload: Option<Payload>,
368}
369
370type Amount = i64;
372
373#[derive(Debug, Deserialize, Serialize, Clone)]
378pub struct PaymentFailedInfo {
379 pub payment_attempts: u32,
381 pub payment_retries_interval: u32,
383}
384
385#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
390#[derive(Debug, Deserialize, Serialize, Clone)]
391pub struct RestoredOrderHelper {
392 pub id: Uuid,
394 pub status: String,
396 pub master_buyer_pubkey: Option<String>,
398 pub master_seller_pubkey: Option<String>,
400 pub trade_index_buyer: Option<i64>,
402 pub trade_index_seller: Option<i64>,
404}
405
406#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
411#[derive(Debug, Deserialize, Serialize, Clone)]
412pub struct RestoredDisputeHelper {
413 pub dispute_id: Uuid,
415 pub order_id: Uuid,
417 pub dispute_status: String,
419 pub master_buyer_pubkey: Option<String>,
421 pub master_seller_pubkey: Option<String>,
423 pub trade_index_buyer: Option<i64>,
425 pub trade_index_seller: Option<i64>,
427 pub buyer_dispute: bool,
431 pub seller_dispute: bool,
435 pub solver_pubkey: Option<String>,
438}
439
440#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
442#[derive(Debug, Deserialize, Serialize, Clone)]
443pub struct RestoredOrdersInfo {
444 pub order_id: Uuid,
446 pub trade_index: i64,
448 pub status: String,
450}
451
452#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
454#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
455#[serde(rename_all = "lowercase")]
456#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
457pub enum DisputeInitiator {
458 Buyer,
460 Seller,
462}
463
464#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
466#[derive(Debug, Deserialize, Serialize, Clone)]
467pub struct RestoredDisputesInfo {
468 pub dispute_id: Uuid,
470 pub order_id: Uuid,
472 pub trade_index: i64,
474 pub status: String,
476 pub initiator: Option<DisputeInitiator>,
479 pub solver_pubkey: Option<String>,
482}
483
484#[derive(Debug, Deserialize, Serialize, Clone, Default)]
489pub struct RestoreSessionInfo {
490 #[serde(rename = "orders")]
492 pub restore_orders: Vec<RestoredOrdersInfo>,
493 #[serde(rename = "disputes")]
495 pub restore_disputes: Vec<RestoredDisputesInfo>,
496}
497
498#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
506pub struct BondResolution {
507 pub slash_seller: bool,
509 pub slash_buyer: bool,
511}
512
513#[derive(Debug, Deserialize, Serialize, Clone)]
519#[serde(rename_all = "snake_case")]
520pub enum Payload {
521 Order(SmallOrder),
523 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
530 TextMessage(String),
532 Peer(Peer),
534 RatingUser(u8),
536 Amount(Amount),
538 Dispute(Uuid, Option<SolverDisputeInfo>),
541 CantDo(Option<CantDoReason>),
543 NextTrade(String, u32),
546 PaymentFailed(PaymentFailedInfo),
548 RestoreData(RestoreSessionInfo),
550 Ids(Vec<Uuid>),
552 Orders(Vec<SmallOrder>),
554 BondResolution(BondResolution),
557}
558
559#[allow(dead_code)]
560impl MessageKind {
561 pub fn new(
564 id: Option<Uuid>,
565 request_id: Option<u64>,
566 trade_index: Option<i64>,
567 action: Action,
568 payload: Option<Payload>,
569 ) -> Self {
570 Self {
571 version: PROTOCOL_VER,
572 request_id,
573 trade_index,
574 id,
575 action,
576 payload,
577 }
578 }
579 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
581 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
582 }
583 pub fn as_json(&self) -> Result<String, ServiceError> {
585 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
586 }
587
588 pub fn get_action(&self) -> Action {
590 self.action.clone()
591 }
592
593 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
600 match &self.payload {
601 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
602 None => Ok(None),
603 _ => Err(ServiceError::InvalidPayload),
604 }
605 }
606
607 pub fn get_rating(&self) -> Result<u8, ServiceError> {
615 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
616 if !(MIN_RATING..=MAX_RATING).contains(&v) {
617 return Err(ServiceError::InvalidRatingValue);
618 }
619 Ok(v)
620 } else {
621 Err(ServiceError::InvalidRating)
622 }
623 }
624
625 pub fn verify(&self) -> bool {
632 match &self.action {
633 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
634 Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
635 if self.id.is_none() {
636 return false;
637 }
638 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
639 }
640 Action::AdminSettle | Action::AdminCancel => {
641 if self.id.is_none() {
642 return false;
643 }
644 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
645 }
646 Action::TakeSell
647 | Action::TakeBuy
648 | Action::FiatSent
649 | Action::FiatSentOk
650 | Action::Release
651 | Action::Released
652 | Action::Dispute
653 | Action::AdminCanceled
654 | Action::AdminSettled
655 | Action::Rate
656 | Action::RateReceived
657 | Action::AdminTakeDispute
658 | Action::AdminTookDispute
659 | Action::DisputeInitiatedByYou
660 | Action::DisputeInitiatedByPeer
661 | Action::WaitingBuyerInvoice
662 | Action::PurchaseCompleted
663 | Action::HoldInvoicePaymentAccepted
664 | Action::HoldInvoicePaymentSettled
665 | Action::HoldInvoicePaymentCanceled
666 | Action::WaitingSellerToPay
667 | Action::BuyerTookOrder
668 | Action::BuyerInvoiceAccepted
669 | Action::CooperativeCancelInitiatedByYou
670 | Action::CooperativeCancelInitiatedByPeer
671 | Action::CooperativeCancelAccepted
672 | Action::Cancel
673 | Action::InvoiceUpdated
674 | Action::AdminAddSolver
675 | Action::SendDm
676 | Action::TradePubkey
677 | Action::Canceled => {
678 if self.id.is_none() {
679 return false;
680 }
681 !matches!(&self.payload, Some(Payload::BondResolution(_)))
682 }
683 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
684 Action::PaymentFailed => {
685 if self.id.is_none() {
686 return false;
687 }
688 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
689 }
690 Action::RateUser => {
691 matches!(&self.payload, Some(Payload::RatingUser(_)))
692 }
693 Action::CantDo => {
694 matches!(&self.payload, Some(Payload::CantDo(_)))
695 }
696 Action::Orders => {
697 matches!(
698 &self.payload,
699 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
700 )
701 }
702 }
703 }
704
705 pub fn get_order(&self) -> Option<&SmallOrder> {
710 if self.action != Action::NewOrder {
711 return None;
712 }
713 match &self.payload {
714 Some(Payload::Order(o)) => Some(o),
715 _ => None,
716 }
717 }
718
719 pub fn get_payment_request(&self) -> Option<String> {
725 if self.action != Action::TakeSell
726 && self.action != Action::AddInvoice
727 && self.action != Action::NewOrder
728 {
729 return None;
730 }
731 match &self.payload {
732 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
733 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
734 _ => None,
735 }
736 }
737
738 pub fn get_amount(&self) -> Option<Amount> {
742 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
743 return None;
744 }
745 match &self.payload {
746 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
747 Some(Payload::Amount(amount)) => Some(*amount),
748 _ => None,
749 }
750 }
751
752 pub fn get_payload(&self) -> Option<&Payload> {
754 self.payload.as_ref()
755 }
756
757 pub fn has_trade_index(&self) -> (bool, i64) {
760 if let Some(index) = self.trade_index {
761 return (true, index);
762 }
763 (false, 0)
764 }
765
766 pub fn trade_index(&self) -> i64 {
768 if let Some(index) = self.trade_index {
769 return index;
770 }
771 0
772 }
773}
774
775#[cfg(test)]
776mod test {
777 use crate::message::{Action, Message, MessageKind, Payload, Peer};
778 use crate::user::UserInfo;
779 use nostr_sdk::Keys;
780 use uuid::uuid;
781
782 #[test]
783 fn test_peer_with_reputation() {
784 let reputation = UserInfo {
786 rating: 4.5,
787 reviews: 10,
788 operating_days: 30,
789 };
790 let peer = Peer::new(
791 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
792 Some(reputation.clone()),
793 );
794
795 assert_eq!(
797 peer.pubkey,
798 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
799 );
800 assert!(peer.reputation.is_some());
801 let peer_reputation = peer.reputation.clone().unwrap();
802 assert_eq!(peer_reputation.rating, 4.5);
803 assert_eq!(peer_reputation.reviews, 10);
804 assert_eq!(peer_reputation.operating_days, 30);
805
806 let json = peer.as_json().unwrap();
808 let deserialized_peer = Peer::from_json(&json).unwrap();
809 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
810 assert!(deserialized_peer.reputation.is_some());
811 let deserialized_reputation = deserialized_peer.reputation.unwrap();
812 assert_eq!(deserialized_reputation.rating, 4.5);
813 assert_eq!(deserialized_reputation.reviews, 10);
814 assert_eq!(deserialized_reputation.operating_days, 30);
815 }
816
817 #[test]
818 fn test_peer_without_reputation() {
819 let peer = Peer::new(
821 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
822 None,
823 );
824
825 assert_eq!(
827 peer.pubkey,
828 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
829 );
830 assert!(peer.reputation.is_none());
831
832 let json = peer.as_json().unwrap();
834 let deserialized_peer = Peer::from_json(&json).unwrap();
835 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
836 assert!(deserialized_peer.reputation.is_none());
837 }
838
839 #[test]
840 fn test_peer_in_message() {
841 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
842
843 let reputation = UserInfo {
845 rating: 4.5,
846 reviews: 10,
847 operating_days: 30,
848 };
849 let peer_with_reputation = Peer::new(
850 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
851 Some(reputation),
852 );
853 let payload_with_reputation = Payload::Peer(peer_with_reputation);
854 let message_with_reputation = Message::Order(MessageKind::new(
855 Some(uuid),
856 Some(1),
857 Some(2),
858 Action::FiatSentOk,
859 Some(payload_with_reputation),
860 ));
861
862 assert!(message_with_reputation.verify());
864 let message_json = message_with_reputation.as_json().unwrap();
865 let deserialized_message = Message::from_json(&message_json).unwrap();
866 assert!(deserialized_message.verify());
867
868 let peer_without_reputation = Peer::new(
870 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
871 None,
872 );
873 let payload_without_reputation = Payload::Peer(peer_without_reputation);
874 let message_without_reputation = Message::Order(MessageKind::new(
875 Some(uuid),
876 Some(1),
877 Some(2),
878 Action::FiatSentOk,
879 Some(payload_without_reputation),
880 ));
881
882 assert!(message_without_reputation.verify());
884 let message_json = message_without_reputation.as_json().unwrap();
885 let deserialized_message = Message::from_json(&message_json).unwrap();
886 assert!(deserialized_message.verify());
887 }
888
889 #[test]
890 fn test_payment_failed_payload() {
891 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
892
893 let payment_failed_info = crate::message::PaymentFailedInfo {
895 payment_attempts: 3,
896 payment_retries_interval: 60,
897 };
898
899 let payload = Payload::PaymentFailed(payment_failed_info);
900 let message = Message::Order(MessageKind::new(
901 Some(uuid),
902 Some(1),
903 Some(2),
904 Action::PaymentFailed,
905 Some(payload),
906 ));
907
908 assert!(message.verify());
910
911 let message_json = message.as_json().unwrap();
913
914 let deserialized_message = Message::from_json(&message_json).unwrap();
916 assert!(deserialized_message.verify());
917
918 if let Message::Order(kind) = deserialized_message {
920 if let Some(Payload::PaymentFailed(info)) = kind.payload {
921 assert_eq!(info.payment_attempts, 3);
922 assert_eq!(info.payment_retries_interval, 60);
923 } else {
924 panic!("Expected PaymentFailed payload");
925 }
926 } else {
927 panic!("Expected Order message");
928 }
929 }
930
931 #[test]
932 fn test_message_payload_signature() {
933 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
934 let peer = Peer::new(
935 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
936 None, );
938 let payload = Payload::Peer(peer);
939 let test_message = Message::Order(MessageKind::new(
940 Some(uuid),
941 Some(1),
942 Some(2),
943 Action::FiatSentOk,
944 Some(payload),
945 ));
946 assert!(test_message.verify());
947 let test_message_json = test_message.as_json().unwrap();
948 let trade_keys =
950 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
951 .unwrap();
952 let sig = Message::sign(test_message_json.clone(), &trade_keys);
953
954 assert!(Message::verify_signature(
955 test_message_json,
956 trade_keys.public_key(),
957 sig
958 ));
959 }
960
961 #[test]
962 fn test_restore_session_message() {
963 let restore_request_message = Message::Restore(MessageKind::new(
965 None,
966 None,
967 None,
968 Action::RestoreSession,
969 None,
970 ));
971
972 assert!(restore_request_message.verify());
974 assert_eq!(
975 restore_request_message.inner_action(),
976 Some(Action::RestoreSession)
977 );
978
979 let message_json = restore_request_message.as_json().unwrap();
981 let deserialized_message = Message::from_json(&message_json).unwrap();
982 assert!(deserialized_message.verify());
983 assert_eq!(
984 deserialized_message.inner_action(),
985 Some(Action::RestoreSession)
986 );
987
988 let restored_orders = vec![
990 crate::message::RestoredOrdersInfo {
991 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
992 trade_index: 1,
993 status: "active".to_string(),
994 },
995 crate::message::RestoredOrdersInfo {
996 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
997 trade_index: 2,
998 status: "success".to_string(),
999 },
1000 ];
1001
1002 let restored_disputes = vec![
1003 crate::message::RestoredDisputesInfo {
1004 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1005 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1006 trade_index: 1,
1007 status: "initiated".to_string(),
1008 initiator: Some(crate::message::DisputeInitiator::Buyer),
1009 solver_pubkey: None,
1010 },
1011 crate::message::RestoredDisputesInfo {
1012 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1013 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1014 trade_index: 2,
1015 status: "in-progress".to_string(),
1016 initiator: None,
1017 solver_pubkey: Some(
1018 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1019 ),
1020 },
1021 crate::message::RestoredDisputesInfo {
1022 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1023 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1024 trade_index: 3,
1025 status: "initiated".to_string(),
1026 initiator: Some(crate::message::DisputeInitiator::Seller),
1027 solver_pubkey: None,
1028 },
1029 ];
1030
1031 let restore_session_info = crate::message::RestoreSessionInfo {
1032 restore_orders: restored_orders.clone(),
1033 restore_disputes: restored_disputes.clone(),
1034 };
1035
1036 let restore_data_payload = Payload::RestoreData(restore_session_info);
1037 let restore_data_message = Message::Restore(MessageKind::new(
1038 None,
1039 None,
1040 None,
1041 Action::RestoreSession,
1042 Some(restore_data_payload),
1043 ));
1044
1045 assert!(!restore_data_message.verify());
1047
1048 let message_json = restore_data_message.as_json().unwrap();
1050 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1051
1052 if let Message::Restore(kind) = deserialized_restore_message {
1053 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1054 assert_eq!(session_info.restore_disputes.len(), 3);
1055 assert_eq!(
1056 session_info.restore_disputes[0].initiator,
1057 Some(crate::message::DisputeInitiator::Buyer)
1058 );
1059 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1060 assert_eq!(session_info.restore_disputes[1].initiator, None);
1061 assert_eq!(
1062 session_info.restore_disputes[1].solver_pubkey,
1063 Some(
1064 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1065 .to_string()
1066 )
1067 );
1068 assert_eq!(
1069 session_info.restore_disputes[2].initiator,
1070 Some(crate::message::DisputeInitiator::Seller)
1071 );
1072 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1073 } else {
1074 panic!("Expected RestoreData payload");
1075 }
1076 } else {
1077 panic!("Expected Restore message");
1078 }
1079 }
1080
1081 #[test]
1082 fn test_restore_session_message_validation() {
1083 let restore_request_message = Message::Restore(MessageKind::new(
1085 None,
1086 None,
1087 None,
1088 Action::RestoreSession,
1089 None, ));
1091
1092 assert!(restore_request_message.verify());
1094
1095 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1097 let wrong_message = Message::Restore(MessageKind::new(
1098 None,
1099 None,
1100 None,
1101 Action::RestoreSession,
1102 Some(wrong_payload),
1103 ));
1104
1105 assert!(!wrong_message.verify());
1107
1108 let with_id = Message::Restore(MessageKind::new(
1110 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1111 None,
1112 None,
1113 Action::RestoreSession,
1114 None,
1115 ));
1116 assert!(with_id.verify());
1117
1118 let with_request_id = Message::Restore(MessageKind::new(
1119 None,
1120 Some(42),
1121 None,
1122 Action::RestoreSession,
1123 None,
1124 ));
1125 assert!(with_request_id.verify());
1126
1127 let with_trade_index = Message::Restore(MessageKind::new(
1128 None,
1129 None,
1130 Some(7),
1131 Action::RestoreSession,
1132 None,
1133 ));
1134 assert!(with_trade_index.verify());
1135 }
1136
1137 #[test]
1138 fn test_restore_session_message_constructor() {
1139 let restore_request_message = Message::new_restore(None);
1141
1142 assert!(matches!(restore_request_message, Message::Restore(_)));
1143 assert!(restore_request_message.verify());
1144 assert_eq!(
1145 restore_request_message.inner_action(),
1146 Some(Action::RestoreSession)
1147 );
1148
1149 let restore_session_info = crate::message::RestoreSessionInfo {
1151 restore_orders: vec![],
1152 restore_disputes: vec![],
1153 };
1154 let restore_data_message =
1155 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1156
1157 assert!(matches!(restore_data_message, Message::Restore(_)));
1158 assert!(!restore_data_message.verify());
1159 }
1160
1161 #[test]
1162 fn test_last_trade_index_valid_message() {
1163 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1164 let msg = Message::Restore(kind);
1165
1166 assert!(msg.verify());
1167
1168 let json = msg.as_json().unwrap();
1170 let decoded = Message::from_json(&json).unwrap();
1171 assert!(decoded.verify());
1172
1173 let inner = decoded.get_inner_message_kind();
1175 assert_eq!(inner.trade_index(), 7);
1176 assert_eq!(inner.has_trade_index(), (true, 7));
1177 }
1178
1179 #[test]
1180 fn test_last_trade_index_without_id_is_valid() {
1181 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1183 let msg = Message::Restore(kind);
1184 assert!(msg.verify());
1185 }
1186
1187 #[test]
1188 fn test_last_trade_index_with_payload_fails_validation() {
1189 let kind = MessageKind::new(
1191 None,
1192 None,
1193 Some(3),
1194 Action::LastTradeIndex,
1195 Some(Payload::TextMessage("ignored".to_string())),
1196 );
1197 let msg = Message::Restore(kind);
1198 assert!(!msg.verify());
1199 }
1200
1201 #[test]
1202 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1203 use crate::message::BondResolution;
1204
1205 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1206
1207 for action in [Action::AdminSettle, Action::AdminCancel] {
1208 let with_resolution = Message::Order(MessageKind::new(
1209 Some(uuid),
1210 Some(1),
1211 Some(2),
1212 action.clone(),
1213 Some(Payload::BondResolution(BondResolution {
1214 slash_seller: true,
1215 slash_buyer: false,
1216 })),
1217 ));
1218 assert!(
1219 with_resolution.verify(),
1220 "{action:?} + BondResolution should verify"
1221 );
1222
1223 let without_payload = Message::Order(MessageKind::new(
1224 Some(uuid),
1225 Some(1),
1226 Some(2),
1227 action.clone(),
1228 None,
1229 ));
1230 assert!(without_payload.verify(), "{action:?} + None should verify");
1231
1232 let wrong = Message::Order(MessageKind::new(
1234 Some(uuid),
1235 Some(1),
1236 Some(2),
1237 action.clone(),
1238 Some(Payload::TextMessage("nope".to_string())),
1239 ));
1240 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1241
1242 let no_id = Message::Order(MessageKind::new(
1244 None,
1245 Some(1),
1246 Some(2),
1247 action,
1248 Some(Payload::BondResolution(BondResolution {
1249 slash_seller: false,
1250 slash_buyer: false,
1251 })),
1252 ));
1253 assert!(!no_id.verify(), "admin action without id must be rejected");
1254 }
1255 }
1256
1257 #[test]
1258 fn test_bond_resolution_rejected_on_non_admin_actions() {
1259 use crate::message::BondResolution;
1260
1261 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1262 let payload = Payload::BondResolution(BondResolution {
1263 slash_seller: true,
1264 slash_buyer: true,
1265 });
1266
1267 for action in [
1271 Action::NewOrder,
1272 Action::TakeSell,
1273 Action::TakeBuy,
1274 Action::PayInvoice,
1275 Action::PayBondInvoice,
1276 Action::FiatSent,
1277 Action::FiatSentOk,
1278 Action::Release,
1279 Action::Released,
1280 Action::Cancel,
1281 Action::Canceled,
1282 Action::CooperativeCancelInitiatedByYou,
1283 Action::CooperativeCancelInitiatedByPeer,
1284 Action::DisputeInitiatedByYou,
1285 Action::DisputeInitiatedByPeer,
1286 Action::CooperativeCancelAccepted,
1287 Action::BuyerInvoiceAccepted,
1288 Action::PurchaseCompleted,
1289 Action::HoldInvoicePaymentAccepted,
1290 Action::HoldInvoicePaymentSettled,
1291 Action::HoldInvoicePaymentCanceled,
1292 Action::WaitingSellerToPay,
1293 Action::WaitingBuyerInvoice,
1294 Action::AddInvoice,
1295 Action::BuyerTookOrder,
1296 Action::Rate,
1297 Action::RateUser,
1298 Action::RateReceived,
1299 Action::CantDo,
1300 Action::Dispute,
1301 Action::AdminCanceled,
1302 Action::AdminSettled,
1303 Action::AdminAddSolver,
1304 Action::AdminTakeDispute,
1305 Action::AdminTookDispute,
1306 Action::PaymentFailed,
1307 Action::InvoiceUpdated,
1308 Action::SendDm,
1309 Action::TradePubkey,
1310 Action::RestoreSession,
1311 Action::LastTradeIndex,
1312 Action::Orders,
1313 ] {
1314 let msg = Message::Order(MessageKind::new(
1315 Some(uuid),
1316 Some(1),
1317 Some(2),
1318 action.clone(),
1319 Some(payload.clone()),
1320 ));
1321 assert!(
1322 !msg.verify(),
1323 "{action:?} must reject BondResolution payload"
1324 );
1325 }
1326 }
1327
1328 #[test]
1329 fn test_bond_resolution_wire_format() {
1330 use crate::message::BondResolution;
1331
1332 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1333 let msg = Message::Order(MessageKind::new(
1334 Some(uuid),
1335 None,
1336 None,
1337 Action::AdminCancel,
1338 Some(Payload::BondResolution(BondResolution {
1339 slash_seller: true,
1340 slash_buyer: false,
1341 })),
1342 ));
1343
1344 let json = msg.as_json().unwrap();
1345 assert!(
1347 json.contains("\"bond_resolution\""),
1348 "expected snake_case discriminator, got: {json}"
1349 );
1350 assert!(json.contains("\"slash_seller\":true"));
1351 assert!(json.contains("\"slash_buyer\":false"));
1352
1353 let decoded = Message::from_json(&json).unwrap();
1355 assert!(decoded.verify());
1356 if let Message::Order(kind) = decoded {
1357 match kind.payload {
1358 Some(Payload::BondResolution(b)) => {
1359 assert!(b.slash_seller);
1360 assert!(!b.slash_buyer);
1361 }
1362 other => panic!("expected BondResolution payload, got {other:?}"),
1363 }
1364 } else {
1365 panic!("expected Order message");
1366 }
1367 }
1368
1369 #[test]
1370 fn test_bond_resolution_legacy_null_payload() {
1371 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1375 let json = format!(
1376 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
1377 );
1378 let msg = Message::from_json(&json).unwrap();
1379 assert!(msg.verify());
1380 }
1381
1382 #[test]
1383 fn test_pay_bond_invoice_wire_format_and_verify() {
1384 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1385 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1386
1387 let msg = Message::Order(MessageKind::new(
1388 Some(uuid),
1389 Some(1),
1390 Some(2),
1391 Action::PayBondInvoice,
1392 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1393 ));
1394 assert!(msg.verify());
1395
1396 let json = msg.as_json().unwrap();
1398 assert!(
1399 json.contains("\"action\":\"pay-bond-invoice\""),
1400 "expected kebab-case discriminator, got: {json}"
1401 );
1402
1403 let decoded = Message::from_json(&json).unwrap();
1405 assert!(decoded.verify());
1406 assert!(matches!(
1407 decoded.inner_action(),
1408 Some(Action::PayBondInvoice)
1409 ));
1410
1411 let no_id = Message::Order(MessageKind::new(
1413 None,
1414 Some(1),
1415 Some(2),
1416 Action::PayBondInvoice,
1417 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1418 ));
1419 assert!(!no_id.verify());
1420
1421 let wrong_payload = Message::Order(MessageKind::new(
1423 Some(uuid),
1424 Some(1),
1425 Some(2),
1426 Action::PayBondInvoice,
1427 Some(Payload::TextMessage("nope".to_string())),
1428 ));
1429 assert!(!wrong_payload.verify());
1430
1431 let no_payload = Message::Order(MessageKind::new(
1433 Some(uuid),
1434 Some(1),
1435 Some(2),
1436 Action::PayBondInvoice,
1437 None,
1438 ));
1439 assert!(!no_payload.verify());
1440 }
1441
1442 #[test]
1443 fn test_restored_dispute_helper_serialization_roundtrip() {
1444 use crate::message::RestoredDisputeHelper;
1445
1446 let helper = RestoredDisputeHelper {
1447 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1448 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1449 dispute_status: "initiated".to_string(),
1450 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
1451 master_seller_pubkey: Some("npub1sellerkey".to_string()),
1452 trade_index_buyer: Some(1),
1453 trade_index_seller: Some(2),
1454 buyer_dispute: true,
1455 seller_dispute: false,
1456 solver_pubkey: None,
1457 };
1458
1459 let json = serde_json::to_string(&helper).unwrap();
1460 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
1461
1462 assert_eq!(deserialized.dispute_id, helper.dispute_id);
1463 assert_eq!(deserialized.order_id, helper.order_id);
1464 assert_eq!(deserialized.dispute_status, helper.dispute_status);
1465 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
1466 assert_eq!(
1467 deserialized.master_seller_pubkey,
1468 helper.master_seller_pubkey
1469 );
1470 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
1471 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
1472 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
1473 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
1474 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
1475
1476 let helper_seller_dispute = RestoredDisputeHelper {
1477 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1478 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1479 dispute_status: "in-progress".to_string(),
1480 master_buyer_pubkey: None,
1481 master_seller_pubkey: None,
1482 trade_index_buyer: None,
1483 trade_index_seller: None,
1484 buyer_dispute: false,
1485 seller_dispute: true,
1486 solver_pubkey: Some(
1487 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1488 ),
1489 };
1490
1491 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
1492 let deserialized_seller: RestoredDisputeHelper =
1493 serde_json::from_str(&json_seller).unwrap();
1494
1495 assert_eq!(
1496 deserialized_seller.dispute_id,
1497 helper_seller_dispute.dispute_id
1498 );
1499 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
1500 assert_eq!(
1501 deserialized_seller.dispute_status,
1502 helper_seller_dispute.dispute_status
1503 );
1504 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
1505 assert_eq!(deserialized_seller.master_seller_pubkey, None);
1506 assert_eq!(deserialized_seller.trade_index_buyer, None);
1507 assert_eq!(deserialized_seller.trade_index_seller, None);
1508 assert!(!deserialized_seller.buyer_dispute);
1509 assert!(deserialized_seller.seller_dispute);
1510 assert_eq!(
1511 deserialized_seller.solver_pubkey,
1512 helper_seller_dispute.solver_pubkey
1513 );
1514 }
1515}