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 AddBondInvoice,
130 BuyerTookOrder,
132 Rate,
134 RateUser,
136 RateReceived,
138 CantDo,
140 Dispute,
142 AdminCancel,
144 AdminCanceled,
146 AdminSettle,
148 AdminSettled,
150 AdminAddSolver,
152 AdminTakeDispute,
154 AdminTookDispute,
156 PaymentFailed,
159 InvoiceUpdated,
161 SendDm,
163 TradePubkey,
165 RestoreSession,
167 LastTradeIndex,
170 Orders,
173}
174
175impl fmt::Display for Action {
176 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177 write!(f, "{self:?}")
178 }
179}
180
181#[derive(Debug, Clone, Deserialize, Serialize)]
188#[serde(rename_all = "kebab-case")]
189pub enum Message {
190 Order(MessageKind),
192 Dispute(MessageKind),
194 CantDo(MessageKind),
196 Rate(MessageKind),
198 Dm(MessageKind),
200 Restore(MessageKind),
202}
203
204impl Message {
205 pub fn new_order(
208 id: Option<Uuid>,
209 request_id: Option<u64>,
210 trade_index: Option<i64>,
211 action: Action,
212 payload: Option<Payload>,
213 ) -> Self {
214 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
215 Self::Order(kind)
216 }
217
218 pub fn new_dispute(
221 id: Option<Uuid>,
222 request_id: Option<u64>,
223 trade_index: Option<i64>,
224 action: Action,
225 payload: Option<Payload>,
226 ) -> Self {
227 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
228
229 Self::Dispute(kind)
230 }
231
232 pub fn new_restore(payload: Option<Payload>) -> Self {
237 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
238 Self::Restore(kind)
239 }
240
241 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
244 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
245
246 Self::CantDo(kind)
247 }
248
249 pub fn new_dm(
251 id: Option<Uuid>,
252 request_id: Option<u64>,
253 action: Action,
254 payload: Option<Payload>,
255 ) -> Self {
256 let kind = MessageKind::new(id, request_id, None, action, payload);
257
258 Self::Dm(kind)
259 }
260
261 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
263 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
264 }
265
266 pub fn as_json(&self) -> Result<String, ServiceError> {
268 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
269 }
270
271 pub fn get_inner_message_kind(&self) -> &MessageKind {
273 match self {
274 Message::Dispute(k)
275 | Message::Order(k)
276 | Message::CantDo(k)
277 | Message::Rate(k)
278 | Message::Dm(k)
279 | Message::Restore(k) => k,
280 }
281 }
282
283 pub fn inner_action(&self) -> Option<Action> {
288 match self {
289 Message::Dispute(a)
290 | Message::Order(a)
291 | Message::CantDo(a)
292 | Message::Rate(a)
293 | Message::Dm(a)
294 | Message::Restore(a) => Some(a.get_action()),
295 }
296 }
297
298 pub fn verify(&self) -> bool {
301 match self {
302 Message::Order(m)
303 | Message::Dispute(m)
304 | Message::CantDo(m)
305 | Message::Rate(m)
306 | Message::Dm(m)
307 | Message::Restore(m) => m.verify(),
308 }
309 }
310
311 pub fn sign(message: String, keys: &Keys) -> Signature {
320 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
321 let hash = hash.to_byte_array();
322 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
323
324 keys.sign_schnorr(&message)
325 }
326
327 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
333 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
335 let hash = hash.to_byte_array();
336 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
337
338 let secp = Secp256k1::verification_only();
340 if let Ok(xonlykey) = pubkey.xonly() {
342 xonlykey.verify(&secp, &message, &sig).is_ok()
343 } else {
344 false
345 }
346 }
347}
348
349#[derive(Debug, Clone, Deserialize, Serialize)]
356pub struct MessageKind {
357 pub version: u8,
360 pub request_id: Option<u64>,
363 pub trade_index: Option<i64>,
366 #[serde(skip_serializing_if = "Option::is_none")]
369 pub id: Option<Uuid>,
370 pub action: Action,
372 pub payload: Option<Payload>,
375}
376
377type Amount = i64;
379
380#[derive(Debug, Deserialize, Serialize, Clone)]
385pub struct PaymentFailedInfo {
386 pub payment_attempts: u32,
388 pub payment_retries_interval: u32,
390}
391
392#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
397#[derive(Debug, Deserialize, Serialize, Clone)]
398pub struct RestoredOrderHelper {
399 pub id: Uuid,
401 pub status: String,
403 pub master_buyer_pubkey: Option<String>,
405 pub master_seller_pubkey: Option<String>,
407 pub trade_index_buyer: Option<i64>,
409 pub trade_index_seller: Option<i64>,
411}
412
413#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
418#[derive(Debug, Deserialize, Serialize, Clone)]
419pub struct RestoredDisputeHelper {
420 pub dispute_id: Uuid,
422 pub order_id: Uuid,
424 pub dispute_status: String,
426 pub master_buyer_pubkey: Option<String>,
428 pub master_seller_pubkey: Option<String>,
430 pub trade_index_buyer: Option<i64>,
432 pub trade_index_seller: Option<i64>,
434 pub buyer_dispute: bool,
438 pub seller_dispute: bool,
442 pub solver_pubkey: Option<String>,
445}
446
447#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
449#[derive(Debug, Deserialize, Serialize, Clone)]
450pub struct RestoredOrdersInfo {
451 pub order_id: Uuid,
453 pub trade_index: i64,
455 pub status: String,
457}
458
459#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
461#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
462#[serde(rename_all = "lowercase")]
463#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
464pub enum DisputeInitiator {
465 Buyer,
467 Seller,
469}
470
471#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
473#[derive(Debug, Deserialize, Serialize, Clone)]
474pub struct RestoredDisputesInfo {
475 pub dispute_id: Uuid,
477 pub order_id: Uuid,
479 pub trade_index: i64,
481 pub status: String,
483 pub initiator: Option<DisputeInitiator>,
486 pub solver_pubkey: Option<String>,
489}
490
491#[derive(Debug, Deserialize, Serialize, Clone, Default)]
496pub struct RestoreSessionInfo {
497 #[serde(rename = "orders")]
499 pub restore_orders: Vec<RestoredOrdersInfo>,
500 #[serde(rename = "disputes")]
502 pub restore_disputes: Vec<RestoredDisputesInfo>,
503}
504
505#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
513pub struct BondResolution {
514 pub slash_seller: bool,
516 pub slash_buyer: bool,
518}
519
520#[derive(Debug, Deserialize, Serialize, Clone)]
526#[serde(rename_all = "snake_case")]
527pub enum Payload {
528 Order(SmallOrder),
530 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
538 TextMessage(String),
540 Peer(Peer),
542 RatingUser(u8),
544 Amount(Amount),
546 Dispute(Uuid, Option<SolverDisputeInfo>),
549 CantDo(Option<CantDoReason>),
551 NextTrade(String, u32),
554 PaymentFailed(PaymentFailedInfo),
556 RestoreData(RestoreSessionInfo),
558 Ids(Vec<Uuid>),
560 Orders(Vec<SmallOrder>),
562 BondResolution(BondResolution),
565}
566
567#[allow(dead_code)]
568impl MessageKind {
569 pub fn new(
572 id: Option<Uuid>,
573 request_id: Option<u64>,
574 trade_index: Option<i64>,
575 action: Action,
576 payload: Option<Payload>,
577 ) -> Self {
578 Self {
579 version: PROTOCOL_VER,
580 request_id,
581 trade_index,
582 id,
583 action,
584 payload,
585 }
586 }
587 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
589 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
590 }
591 pub fn as_json(&self) -> Result<String, ServiceError> {
593 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
594 }
595
596 pub fn get_action(&self) -> Action {
598 self.action.clone()
599 }
600
601 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
608 match &self.payload {
609 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
610 None => Ok(None),
611 _ => Err(ServiceError::InvalidPayload),
612 }
613 }
614
615 pub fn get_rating(&self) -> Result<u8, ServiceError> {
623 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
624 if !(MIN_RATING..=MAX_RATING).contains(&v) {
625 return Err(ServiceError::InvalidRatingValue);
626 }
627 Ok(v)
628 } else {
629 Err(ServiceError::InvalidRating)
630 }
631 }
632
633 pub fn verify(&self) -> bool {
640 match &self.action {
641 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
642 Action::PayInvoice
643 | Action::PayBondInvoice
644 | Action::AddInvoice
645 | Action::AddBondInvoice => {
646 if self.id.is_none() {
647 return false;
648 }
649 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
650 }
651 Action::AdminSettle | Action::AdminCancel => {
652 if self.id.is_none() {
653 return false;
654 }
655 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
656 }
657 Action::TakeSell
658 | Action::TakeBuy
659 | Action::FiatSent
660 | Action::FiatSentOk
661 | Action::Release
662 | Action::Released
663 | Action::Dispute
664 | Action::AdminCanceled
665 | Action::AdminSettled
666 | Action::Rate
667 | Action::RateReceived
668 | Action::AdminTakeDispute
669 | Action::AdminTookDispute
670 | Action::DisputeInitiatedByYou
671 | Action::DisputeInitiatedByPeer
672 | Action::WaitingBuyerInvoice
673 | Action::PurchaseCompleted
674 | Action::HoldInvoicePaymentAccepted
675 | Action::HoldInvoicePaymentSettled
676 | Action::HoldInvoicePaymentCanceled
677 | Action::WaitingSellerToPay
678 | Action::BuyerTookOrder
679 | Action::BuyerInvoiceAccepted
680 | Action::CooperativeCancelInitiatedByYou
681 | Action::CooperativeCancelInitiatedByPeer
682 | Action::CooperativeCancelAccepted
683 | Action::Cancel
684 | Action::InvoiceUpdated
685 | Action::AdminAddSolver
686 | Action::SendDm
687 | Action::TradePubkey
688 | Action::Canceled => {
689 if self.id.is_none() {
690 return false;
691 }
692 !matches!(&self.payload, Some(Payload::BondResolution(_)))
693 }
694 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
695 Action::PaymentFailed => {
696 if self.id.is_none() {
697 return false;
698 }
699 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
700 }
701 Action::RateUser => {
702 matches!(&self.payload, Some(Payload::RatingUser(_)))
703 }
704 Action::CantDo => {
705 matches!(&self.payload, Some(Payload::CantDo(_)))
706 }
707 Action::Orders => {
708 matches!(
709 &self.payload,
710 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
711 )
712 }
713 }
714 }
715
716 pub fn get_order(&self) -> Option<&SmallOrder> {
721 if self.action != Action::NewOrder {
722 return None;
723 }
724 match &self.payload {
725 Some(Payload::Order(o)) => Some(o),
726 _ => None,
727 }
728 }
729
730 pub fn get_payment_request(&self) -> Option<String> {
737 if self.action != Action::TakeSell
738 && self.action != Action::AddInvoice
739 && self.action != Action::AddBondInvoice
740 && self.action != Action::NewOrder
741 {
742 return None;
743 }
744 match &self.payload {
745 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
746 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
747 _ => None,
748 }
749 }
750
751 pub fn get_amount(&self) -> Option<Amount> {
755 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
756 return None;
757 }
758 match &self.payload {
759 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
760 Some(Payload::Amount(amount)) => Some(*amount),
761 _ => None,
762 }
763 }
764
765 pub fn get_payload(&self) -> Option<&Payload> {
767 self.payload.as_ref()
768 }
769
770 pub fn has_trade_index(&self) -> (bool, i64) {
773 if let Some(index) = self.trade_index {
774 return (true, index);
775 }
776 (false, 0)
777 }
778
779 pub fn trade_index(&self) -> i64 {
781 if let Some(index) = self.trade_index {
782 return index;
783 }
784 0
785 }
786}
787
788#[cfg(test)]
789mod test {
790 use crate::message::{Action, Message, MessageKind, Payload, Peer};
791 use crate::user::UserInfo;
792 use nostr_sdk::Keys;
793 use uuid::uuid;
794
795 #[test]
796 fn test_peer_with_reputation() {
797 let reputation = UserInfo {
799 rating: 4.5,
800 reviews: 10,
801 operating_days: 30,
802 };
803 let peer = Peer::new(
804 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
805 Some(reputation.clone()),
806 );
807
808 assert_eq!(
810 peer.pubkey,
811 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
812 );
813 assert!(peer.reputation.is_some());
814 let peer_reputation = peer.reputation.clone().unwrap();
815 assert_eq!(peer_reputation.rating, 4.5);
816 assert_eq!(peer_reputation.reviews, 10);
817 assert_eq!(peer_reputation.operating_days, 30);
818
819 let json = peer.as_json().unwrap();
821 let deserialized_peer = Peer::from_json(&json).unwrap();
822 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
823 assert!(deserialized_peer.reputation.is_some());
824 let deserialized_reputation = deserialized_peer.reputation.unwrap();
825 assert_eq!(deserialized_reputation.rating, 4.5);
826 assert_eq!(deserialized_reputation.reviews, 10);
827 assert_eq!(deserialized_reputation.operating_days, 30);
828 }
829
830 #[test]
831 fn test_peer_without_reputation() {
832 let peer = Peer::new(
834 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
835 None,
836 );
837
838 assert_eq!(
840 peer.pubkey,
841 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
842 );
843 assert!(peer.reputation.is_none());
844
845 let json = peer.as_json().unwrap();
847 let deserialized_peer = Peer::from_json(&json).unwrap();
848 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
849 assert!(deserialized_peer.reputation.is_none());
850 }
851
852 #[test]
853 fn test_peer_in_message() {
854 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
855
856 let reputation = UserInfo {
858 rating: 4.5,
859 reviews: 10,
860 operating_days: 30,
861 };
862 let peer_with_reputation = Peer::new(
863 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
864 Some(reputation),
865 );
866 let payload_with_reputation = Payload::Peer(peer_with_reputation);
867 let message_with_reputation = Message::Order(MessageKind::new(
868 Some(uuid),
869 Some(1),
870 Some(2),
871 Action::FiatSentOk,
872 Some(payload_with_reputation),
873 ));
874
875 assert!(message_with_reputation.verify());
877 let message_json = message_with_reputation.as_json().unwrap();
878 let deserialized_message = Message::from_json(&message_json).unwrap();
879 assert!(deserialized_message.verify());
880
881 let peer_without_reputation = Peer::new(
883 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
884 None,
885 );
886 let payload_without_reputation = Payload::Peer(peer_without_reputation);
887 let message_without_reputation = Message::Order(MessageKind::new(
888 Some(uuid),
889 Some(1),
890 Some(2),
891 Action::FiatSentOk,
892 Some(payload_without_reputation),
893 ));
894
895 assert!(message_without_reputation.verify());
897 let message_json = message_without_reputation.as_json().unwrap();
898 let deserialized_message = Message::from_json(&message_json).unwrap();
899 assert!(deserialized_message.verify());
900 }
901
902 #[test]
903 fn test_payment_failed_payload() {
904 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
905
906 let payment_failed_info = crate::message::PaymentFailedInfo {
908 payment_attempts: 3,
909 payment_retries_interval: 60,
910 };
911
912 let payload = Payload::PaymentFailed(payment_failed_info);
913 let message = Message::Order(MessageKind::new(
914 Some(uuid),
915 Some(1),
916 Some(2),
917 Action::PaymentFailed,
918 Some(payload),
919 ));
920
921 assert!(message.verify());
923
924 let message_json = message.as_json().unwrap();
926
927 let deserialized_message = Message::from_json(&message_json).unwrap();
929 assert!(deserialized_message.verify());
930
931 if let Message::Order(kind) = deserialized_message {
933 if let Some(Payload::PaymentFailed(info)) = kind.payload {
934 assert_eq!(info.payment_attempts, 3);
935 assert_eq!(info.payment_retries_interval, 60);
936 } else {
937 panic!("Expected PaymentFailed payload");
938 }
939 } else {
940 panic!("Expected Order message");
941 }
942 }
943
944 #[test]
945 fn test_message_payload_signature() {
946 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
947 let peer = Peer::new(
948 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
949 None, );
951 let payload = Payload::Peer(peer);
952 let test_message = Message::Order(MessageKind::new(
953 Some(uuid),
954 Some(1),
955 Some(2),
956 Action::FiatSentOk,
957 Some(payload),
958 ));
959 assert!(test_message.verify());
960 let test_message_json = test_message.as_json().unwrap();
961 let trade_keys =
963 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
964 .unwrap();
965 let sig = Message::sign(test_message_json.clone(), &trade_keys);
966
967 assert!(Message::verify_signature(
968 test_message_json,
969 trade_keys.public_key(),
970 sig
971 ));
972 }
973
974 #[test]
975 fn test_restore_session_message() {
976 let restore_request_message = Message::Restore(MessageKind::new(
978 None,
979 None,
980 None,
981 Action::RestoreSession,
982 None,
983 ));
984
985 assert!(restore_request_message.verify());
987 assert_eq!(
988 restore_request_message.inner_action(),
989 Some(Action::RestoreSession)
990 );
991
992 let message_json = restore_request_message.as_json().unwrap();
994 let deserialized_message = Message::from_json(&message_json).unwrap();
995 assert!(deserialized_message.verify());
996 assert_eq!(
997 deserialized_message.inner_action(),
998 Some(Action::RestoreSession)
999 );
1000
1001 let restored_orders = vec![
1003 crate::message::RestoredOrdersInfo {
1004 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1005 trade_index: 1,
1006 status: "active".to_string(),
1007 },
1008 crate::message::RestoredOrdersInfo {
1009 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1010 trade_index: 2,
1011 status: "success".to_string(),
1012 },
1013 ];
1014
1015 let restored_disputes = vec![
1016 crate::message::RestoredDisputesInfo {
1017 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1018 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1019 trade_index: 1,
1020 status: "initiated".to_string(),
1021 initiator: Some(crate::message::DisputeInitiator::Buyer),
1022 solver_pubkey: None,
1023 },
1024 crate::message::RestoredDisputesInfo {
1025 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1026 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1027 trade_index: 2,
1028 status: "in-progress".to_string(),
1029 initiator: None,
1030 solver_pubkey: Some(
1031 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1032 ),
1033 },
1034 crate::message::RestoredDisputesInfo {
1035 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1036 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1037 trade_index: 3,
1038 status: "initiated".to_string(),
1039 initiator: Some(crate::message::DisputeInitiator::Seller),
1040 solver_pubkey: None,
1041 },
1042 ];
1043
1044 let restore_session_info = crate::message::RestoreSessionInfo {
1045 restore_orders: restored_orders.clone(),
1046 restore_disputes: restored_disputes.clone(),
1047 };
1048
1049 let restore_data_payload = Payload::RestoreData(restore_session_info);
1050 let restore_data_message = Message::Restore(MessageKind::new(
1051 None,
1052 None,
1053 None,
1054 Action::RestoreSession,
1055 Some(restore_data_payload),
1056 ));
1057
1058 assert!(!restore_data_message.verify());
1060
1061 let message_json = restore_data_message.as_json().unwrap();
1063 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1064
1065 if let Message::Restore(kind) = deserialized_restore_message {
1066 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1067 assert_eq!(session_info.restore_disputes.len(), 3);
1068 assert_eq!(
1069 session_info.restore_disputes[0].initiator,
1070 Some(crate::message::DisputeInitiator::Buyer)
1071 );
1072 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1073 assert_eq!(session_info.restore_disputes[1].initiator, None);
1074 assert_eq!(
1075 session_info.restore_disputes[1].solver_pubkey,
1076 Some(
1077 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1078 .to_string()
1079 )
1080 );
1081 assert_eq!(
1082 session_info.restore_disputes[2].initiator,
1083 Some(crate::message::DisputeInitiator::Seller)
1084 );
1085 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1086 } else {
1087 panic!("Expected RestoreData payload");
1088 }
1089 } else {
1090 panic!("Expected Restore message");
1091 }
1092 }
1093
1094 #[test]
1095 fn test_restore_session_message_validation() {
1096 let restore_request_message = Message::Restore(MessageKind::new(
1098 None,
1099 None,
1100 None,
1101 Action::RestoreSession,
1102 None, ));
1104
1105 assert!(restore_request_message.verify());
1107
1108 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1110 let wrong_message = Message::Restore(MessageKind::new(
1111 None,
1112 None,
1113 None,
1114 Action::RestoreSession,
1115 Some(wrong_payload),
1116 ));
1117
1118 assert!(!wrong_message.verify());
1120
1121 let with_id = Message::Restore(MessageKind::new(
1123 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1124 None,
1125 None,
1126 Action::RestoreSession,
1127 None,
1128 ));
1129 assert!(with_id.verify());
1130
1131 let with_request_id = Message::Restore(MessageKind::new(
1132 None,
1133 Some(42),
1134 None,
1135 Action::RestoreSession,
1136 None,
1137 ));
1138 assert!(with_request_id.verify());
1139
1140 let with_trade_index = Message::Restore(MessageKind::new(
1141 None,
1142 None,
1143 Some(7),
1144 Action::RestoreSession,
1145 None,
1146 ));
1147 assert!(with_trade_index.verify());
1148 }
1149
1150 #[test]
1151 fn test_restore_session_message_constructor() {
1152 let restore_request_message = Message::new_restore(None);
1154
1155 assert!(matches!(restore_request_message, Message::Restore(_)));
1156 assert!(restore_request_message.verify());
1157 assert_eq!(
1158 restore_request_message.inner_action(),
1159 Some(Action::RestoreSession)
1160 );
1161
1162 let restore_session_info = crate::message::RestoreSessionInfo {
1164 restore_orders: vec![],
1165 restore_disputes: vec![],
1166 };
1167 let restore_data_message =
1168 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1169
1170 assert!(matches!(restore_data_message, Message::Restore(_)));
1171 assert!(!restore_data_message.verify());
1172 }
1173
1174 #[test]
1175 fn test_last_trade_index_valid_message() {
1176 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1177 let msg = Message::Restore(kind);
1178
1179 assert!(msg.verify());
1180
1181 let json = msg.as_json().unwrap();
1183 let decoded = Message::from_json(&json).unwrap();
1184 assert!(decoded.verify());
1185
1186 let inner = decoded.get_inner_message_kind();
1188 assert_eq!(inner.trade_index(), 7);
1189 assert_eq!(inner.has_trade_index(), (true, 7));
1190 }
1191
1192 #[test]
1193 fn test_last_trade_index_without_id_is_valid() {
1194 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1196 let msg = Message::Restore(kind);
1197 assert!(msg.verify());
1198 }
1199
1200 #[test]
1201 fn test_last_trade_index_with_payload_fails_validation() {
1202 let kind = MessageKind::new(
1204 None,
1205 None,
1206 Some(3),
1207 Action::LastTradeIndex,
1208 Some(Payload::TextMessage("ignored".to_string())),
1209 );
1210 let msg = Message::Restore(kind);
1211 assert!(!msg.verify());
1212 }
1213
1214 #[test]
1215 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1216 use crate::message::BondResolution;
1217
1218 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1219
1220 for action in [Action::AdminSettle, Action::AdminCancel] {
1221 let with_resolution = Message::Order(MessageKind::new(
1222 Some(uuid),
1223 Some(1),
1224 Some(2),
1225 action.clone(),
1226 Some(Payload::BondResolution(BondResolution {
1227 slash_seller: true,
1228 slash_buyer: false,
1229 })),
1230 ));
1231 assert!(
1232 with_resolution.verify(),
1233 "{action:?} + BondResolution should verify"
1234 );
1235
1236 let without_payload = Message::Order(MessageKind::new(
1237 Some(uuid),
1238 Some(1),
1239 Some(2),
1240 action.clone(),
1241 None,
1242 ));
1243 assert!(without_payload.verify(), "{action:?} + None should verify");
1244
1245 let wrong = Message::Order(MessageKind::new(
1247 Some(uuid),
1248 Some(1),
1249 Some(2),
1250 action.clone(),
1251 Some(Payload::TextMessage("nope".to_string())),
1252 ));
1253 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1254
1255 let no_id = Message::Order(MessageKind::new(
1257 None,
1258 Some(1),
1259 Some(2),
1260 action,
1261 Some(Payload::BondResolution(BondResolution {
1262 slash_seller: false,
1263 slash_buyer: false,
1264 })),
1265 ));
1266 assert!(!no_id.verify(), "admin action without id must be rejected");
1267 }
1268 }
1269
1270 #[test]
1271 fn test_bond_resolution_rejected_on_non_admin_actions() {
1272 use crate::message::BondResolution;
1273
1274 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1275 let payload = Payload::BondResolution(BondResolution {
1276 slash_seller: true,
1277 slash_buyer: true,
1278 });
1279
1280 for action in [
1284 Action::NewOrder,
1285 Action::TakeSell,
1286 Action::TakeBuy,
1287 Action::PayInvoice,
1288 Action::PayBondInvoice,
1289 Action::FiatSent,
1290 Action::FiatSentOk,
1291 Action::Release,
1292 Action::Released,
1293 Action::Cancel,
1294 Action::Canceled,
1295 Action::CooperativeCancelInitiatedByYou,
1296 Action::CooperativeCancelInitiatedByPeer,
1297 Action::DisputeInitiatedByYou,
1298 Action::DisputeInitiatedByPeer,
1299 Action::CooperativeCancelAccepted,
1300 Action::BuyerInvoiceAccepted,
1301 Action::PurchaseCompleted,
1302 Action::HoldInvoicePaymentAccepted,
1303 Action::HoldInvoicePaymentSettled,
1304 Action::HoldInvoicePaymentCanceled,
1305 Action::WaitingSellerToPay,
1306 Action::WaitingBuyerInvoice,
1307 Action::AddInvoice,
1308 Action::AddBondInvoice,
1309 Action::BuyerTookOrder,
1310 Action::Rate,
1311 Action::RateUser,
1312 Action::RateReceived,
1313 Action::CantDo,
1314 Action::Dispute,
1315 Action::AdminCanceled,
1316 Action::AdminSettled,
1317 Action::AdminAddSolver,
1318 Action::AdminTakeDispute,
1319 Action::AdminTookDispute,
1320 Action::PaymentFailed,
1321 Action::InvoiceUpdated,
1322 Action::SendDm,
1323 Action::TradePubkey,
1324 Action::RestoreSession,
1325 Action::LastTradeIndex,
1326 Action::Orders,
1327 ] {
1328 let msg = Message::Order(MessageKind::new(
1329 Some(uuid),
1330 Some(1),
1331 Some(2),
1332 action.clone(),
1333 Some(payload.clone()),
1334 ));
1335 assert!(
1336 !msg.verify(),
1337 "{action:?} must reject BondResolution payload"
1338 );
1339 }
1340 }
1341
1342 #[test]
1343 fn test_bond_resolution_wire_format() {
1344 use crate::message::BondResolution;
1345
1346 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1347 let msg = Message::Order(MessageKind::new(
1348 Some(uuid),
1349 None,
1350 None,
1351 Action::AdminCancel,
1352 Some(Payload::BondResolution(BondResolution {
1353 slash_seller: true,
1354 slash_buyer: false,
1355 })),
1356 ));
1357
1358 let json = msg.as_json().unwrap();
1359 assert!(
1361 json.contains("\"bond_resolution\""),
1362 "expected snake_case discriminator, got: {json}"
1363 );
1364 assert!(json.contains("\"slash_seller\":true"));
1365 assert!(json.contains("\"slash_buyer\":false"));
1366
1367 let decoded = Message::from_json(&json).unwrap();
1369 assert!(decoded.verify());
1370 if let Message::Order(kind) = decoded {
1371 match kind.payload {
1372 Some(Payload::BondResolution(b)) => {
1373 assert!(b.slash_seller);
1374 assert!(!b.slash_buyer);
1375 }
1376 other => panic!("expected BondResolution payload, got {other:?}"),
1377 }
1378 } else {
1379 panic!("expected Order message");
1380 }
1381 }
1382
1383 #[test]
1384 fn test_bond_resolution_legacy_null_payload() {
1385 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1389 let json = format!(
1390 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
1391 );
1392 let msg = Message::from_json(&json).unwrap();
1393 assert!(msg.verify());
1394 }
1395
1396 #[test]
1397 fn test_pay_bond_invoice_wire_format_and_verify() {
1398 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1399 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1400
1401 let msg = Message::Order(MessageKind::new(
1402 Some(uuid),
1403 Some(1),
1404 Some(2),
1405 Action::PayBondInvoice,
1406 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1407 ));
1408 assert!(msg.verify());
1409
1410 let json = msg.as_json().unwrap();
1412 assert!(
1413 json.contains("\"action\":\"pay-bond-invoice\""),
1414 "expected kebab-case discriminator, got: {json}"
1415 );
1416
1417 let decoded = Message::from_json(&json).unwrap();
1419 assert!(decoded.verify());
1420 assert!(matches!(
1421 decoded.inner_action(),
1422 Some(Action::PayBondInvoice)
1423 ));
1424
1425 let no_id = Message::Order(MessageKind::new(
1427 None,
1428 Some(1),
1429 Some(2),
1430 Action::PayBondInvoice,
1431 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1432 ));
1433 assert!(!no_id.verify());
1434
1435 let wrong_payload = Message::Order(MessageKind::new(
1437 Some(uuid),
1438 Some(1),
1439 Some(2),
1440 Action::PayBondInvoice,
1441 Some(Payload::TextMessage("nope".to_string())),
1442 ));
1443 assert!(!wrong_payload.verify());
1444
1445 let no_payload = Message::Order(MessageKind::new(
1447 Some(uuid),
1448 Some(1),
1449 Some(2),
1450 Action::PayBondInvoice,
1451 None,
1452 ));
1453 assert!(!no_payload.verify());
1454 }
1455
1456 #[test]
1457 fn test_add_bond_invoice_wire_format_and_verify() {
1458 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1459 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1460
1461 let msg = Message::Order(MessageKind::new(
1462 Some(uuid),
1463 Some(1),
1464 Some(2),
1465 Action::AddBondInvoice,
1466 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1467 ));
1468 assert!(msg.verify());
1469
1470 let json = msg.as_json().unwrap();
1472 assert!(
1473 json.contains("\"action\":\"add-bond-invoice\""),
1474 "expected kebab-case discriminator, got: {json}"
1475 );
1476
1477 let decoded = Message::from_json(&json).unwrap();
1479 assert!(decoded.verify());
1480 assert!(matches!(
1481 decoded.inner_action(),
1482 Some(Action::AddBondInvoice)
1483 ));
1484
1485 let no_id = Message::Order(MessageKind::new(
1487 None,
1488 Some(1),
1489 Some(2),
1490 Action::AddBondInvoice,
1491 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1492 ));
1493 assert!(!no_id.verify());
1494
1495 let wrong_payload = Message::Order(MessageKind::new(
1497 Some(uuid),
1498 Some(1),
1499 Some(2),
1500 Action::AddBondInvoice,
1501 Some(Payload::TextMessage("nope".to_string())),
1502 ));
1503 assert!(!wrong_payload.verify());
1504
1505 let no_payload = Message::Order(MessageKind::new(
1507 Some(uuid),
1508 Some(1),
1509 Some(2),
1510 Action::AddBondInvoice,
1511 None,
1512 ));
1513 assert!(!no_payload.verify());
1514
1515 if let Message::Order(kind) = &msg {
1517 assert_eq!(kind.get_payment_request(), Some(bolt11));
1518 } else {
1519 panic!("expected Message::Order");
1520 }
1521 }
1522
1523 #[test]
1524 fn test_restored_dispute_helper_serialization_roundtrip() {
1525 use crate::message::RestoredDisputeHelper;
1526
1527 let helper = RestoredDisputeHelper {
1528 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1529 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1530 dispute_status: "initiated".to_string(),
1531 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
1532 master_seller_pubkey: Some("npub1sellerkey".to_string()),
1533 trade_index_buyer: Some(1),
1534 trade_index_seller: Some(2),
1535 buyer_dispute: true,
1536 seller_dispute: false,
1537 solver_pubkey: None,
1538 };
1539
1540 let json = serde_json::to_string(&helper).unwrap();
1541 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
1542
1543 assert_eq!(deserialized.dispute_id, helper.dispute_id);
1544 assert_eq!(deserialized.order_id, helper.order_id);
1545 assert_eq!(deserialized.dispute_status, helper.dispute_status);
1546 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
1547 assert_eq!(
1548 deserialized.master_seller_pubkey,
1549 helper.master_seller_pubkey
1550 );
1551 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
1552 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
1553 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
1554 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
1555 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
1556
1557 let helper_seller_dispute = RestoredDisputeHelper {
1558 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1559 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1560 dispute_status: "in-progress".to_string(),
1561 master_buyer_pubkey: None,
1562 master_seller_pubkey: None,
1563 trade_index_buyer: None,
1564 trade_index_seller: None,
1565 buyer_dispute: false,
1566 seller_dispute: true,
1567 solver_pubkey: Some(
1568 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1569 ),
1570 };
1571
1572 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
1573 let deserialized_seller: RestoredDisputeHelper =
1574 serde_json::from_str(&json_seller).unwrap();
1575
1576 assert_eq!(
1577 deserialized_seller.dispute_id,
1578 helper_seller_dispute.dispute_id
1579 );
1580 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
1581 assert_eq!(
1582 deserialized_seller.dispute_status,
1583 helper_seller_dispute.dispute_status
1584 );
1585 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
1586 assert_eq!(deserialized_seller.master_seller_pubkey, None);
1587 assert_eq!(deserialized_seller.trade_index_buyer, None);
1588 assert_eq!(deserialized_seller.trade_index_seller, None);
1589 assert!(!deserialized_seller.buyer_dispute);
1590 assert!(deserialized_seller.seller_dispute);
1591 assert_eq!(
1592 deserialized_seller.solver_pubkey,
1593 helper_seller_dispute.solver_pubkey
1594 );
1595 }
1596}