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 BondInvoiceAccepted,
114 PurchaseCompleted,
116 BondPayoutCompleted,
122 HoldInvoicePaymentAccepted,
124 HoldInvoicePaymentSettled,
126 HoldInvoicePaymentCanceled,
128 WaitingSellerToPay,
130 WaitingBuyerInvoice,
132 AddInvoice,
135 AddBondInvoice,
142 BuyerTookOrder,
144 Rate,
146 RateUser,
148 RateReceived,
150 CantDo,
152 Dispute,
154 AdminCancel,
156 AdminCanceled,
158 AdminSettle,
160 AdminSettled,
162 AdminAddSolver,
164 AdminTakeDispute,
166 AdminTookDispute,
168 PaymentFailed,
171 InvoiceUpdated,
173 SendDm,
175 TradePubkey,
177 RestoreSession,
179 LastTradeIndex,
182 Orders,
185}
186
187impl fmt::Display for Action {
188 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
189 write!(f, "{self:?}")
190 }
191}
192
193#[derive(Debug, Clone, Deserialize, Serialize)]
200#[serde(rename_all = "kebab-case")]
201pub enum Message {
202 Order(MessageKind),
204 Dispute(MessageKind),
206 CantDo(MessageKind),
208 Rate(MessageKind),
210 Dm(MessageKind),
212 Restore(MessageKind),
214}
215
216impl Message {
217 pub fn new_order(
220 id: Option<Uuid>,
221 request_id: Option<u64>,
222 trade_index: Option<i64>,
223 action: Action,
224 payload: Option<Payload>,
225 ) -> Self {
226 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
227 Self::Order(kind)
228 }
229
230 pub fn new_dispute(
233 id: Option<Uuid>,
234 request_id: Option<u64>,
235 trade_index: Option<i64>,
236 action: Action,
237 payload: Option<Payload>,
238 ) -> Self {
239 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
240
241 Self::Dispute(kind)
242 }
243
244 pub fn new_restore(payload: Option<Payload>) -> Self {
249 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
250 Self::Restore(kind)
251 }
252
253 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
256 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
257
258 Self::CantDo(kind)
259 }
260
261 pub fn new_dm(
263 id: Option<Uuid>,
264 request_id: Option<u64>,
265 action: Action,
266 payload: Option<Payload>,
267 ) -> Self {
268 let kind = MessageKind::new(id, request_id, None, action, payload);
269
270 Self::Dm(kind)
271 }
272
273 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
275 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
276 }
277
278 pub fn as_json(&self) -> Result<String, ServiceError> {
280 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
281 }
282
283 pub fn get_inner_message_kind(&self) -> &MessageKind {
285 match self {
286 Message::Dispute(k)
287 | Message::Order(k)
288 | Message::CantDo(k)
289 | Message::Rate(k)
290 | Message::Dm(k)
291 | Message::Restore(k) => k,
292 }
293 }
294
295 pub fn inner_action(&self) -> Option<Action> {
300 match self {
301 Message::Dispute(a)
302 | Message::Order(a)
303 | Message::CantDo(a)
304 | Message::Rate(a)
305 | Message::Dm(a)
306 | Message::Restore(a) => Some(a.get_action()),
307 }
308 }
309
310 pub fn verify(&self) -> bool {
313 match self {
314 Message::Order(m)
315 | Message::Dispute(m)
316 | Message::CantDo(m)
317 | Message::Rate(m)
318 | Message::Dm(m)
319 | Message::Restore(m) => m.verify(),
320 }
321 }
322
323 pub fn sign(message: String, keys: &Keys) -> Signature {
332 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
333 let hash = hash.to_byte_array();
334 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
335
336 keys.sign_schnorr(&message)
337 }
338
339 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
345 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
347 let hash = hash.to_byte_array();
348 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
349
350 let secp = Secp256k1::verification_only();
352 if let Ok(xonlykey) = pubkey.xonly() {
354 xonlykey.verify(&secp, &message, &sig).is_ok()
355 } else {
356 false
357 }
358 }
359}
360
361#[derive(Debug, Clone, Deserialize, Serialize)]
368pub struct MessageKind {
369 pub version: u8,
372 pub request_id: Option<u64>,
375 pub trade_index: Option<i64>,
378 #[serde(skip_serializing_if = "Option::is_none")]
381 pub id: Option<Uuid>,
382 pub action: Action,
384 pub payload: Option<Payload>,
387}
388
389type Amount = i64;
391
392#[derive(Debug, Deserialize, Serialize, Clone)]
397pub struct PaymentFailedInfo {
398 pub payment_attempts: u32,
400 pub payment_retries_interval: u32,
402}
403
404#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
409#[derive(Debug, Deserialize, Serialize, Clone)]
410pub struct RestoredOrderHelper {
411 pub id: Uuid,
413 pub status: String,
415 pub master_buyer_pubkey: Option<String>,
417 pub master_seller_pubkey: Option<String>,
419 pub trade_index_buyer: Option<i64>,
421 pub trade_index_seller: Option<i64>,
423}
424
425#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
430#[derive(Debug, Deserialize, Serialize, Clone)]
431pub struct RestoredDisputeHelper {
432 pub dispute_id: Uuid,
434 pub order_id: Uuid,
436 pub dispute_status: String,
438 pub master_buyer_pubkey: Option<String>,
440 pub master_seller_pubkey: Option<String>,
442 pub trade_index_buyer: Option<i64>,
444 pub trade_index_seller: Option<i64>,
446 pub buyer_dispute: bool,
450 pub seller_dispute: bool,
454 pub solver_pubkey: Option<String>,
457}
458
459#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
461#[derive(Debug, Deserialize, Serialize, Clone)]
462pub struct RestoredOrdersInfo {
463 pub order_id: Uuid,
465 pub trade_index: i64,
467 pub status: String,
469}
470
471#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
473#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
474#[serde(rename_all = "lowercase")]
475#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
476pub enum DisputeInitiator {
477 Buyer,
479 Seller,
481}
482
483#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
485#[derive(Debug, Deserialize, Serialize, Clone)]
486pub struct RestoredDisputesInfo {
487 pub dispute_id: Uuid,
489 pub order_id: Uuid,
491 pub trade_index: i64,
493 pub status: String,
495 pub initiator: Option<DisputeInitiator>,
498 pub solver_pubkey: Option<String>,
501}
502
503#[derive(Debug, Deserialize, Serialize, Clone, Default)]
508pub struct RestoreSessionInfo {
509 #[serde(rename = "orders")]
511 pub restore_orders: Vec<RestoredOrdersInfo>,
512 #[serde(rename = "disputes")]
514 pub restore_disputes: Vec<RestoredDisputesInfo>,
515}
516
517#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
525pub struct BondResolution {
526 pub slash_seller: bool,
528 pub slash_buyer: bool,
530}
531
532#[derive(Debug, Deserialize, Serialize, Clone)]
547pub struct BondPayoutRequest {
548 pub order: SmallOrder,
552 pub slashed_at: i64,
557}
558
559#[derive(Debug, Deserialize, Serialize, Clone)]
565#[serde(rename_all = "snake_case")]
566pub enum Payload {
567 Order(SmallOrder),
569 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
577 TextMessage(String),
579 Peer(Peer),
581 RatingUser(u8),
583 Amount(Amount),
585 Dispute(Uuid, Option<SolverDisputeInfo>),
588 CantDo(Option<CantDoReason>),
590 NextTrade(String, u32),
593 PaymentFailed(PaymentFailedInfo),
595 RestoreData(RestoreSessionInfo),
597 Ids(Vec<Uuid>),
599 Orders(Vec<SmallOrder>),
601 BondResolution(BondResolution),
604 BondPayoutRequest(BondPayoutRequest),
610}
611
612#[allow(dead_code)]
613impl MessageKind {
614 pub fn new(
617 id: Option<Uuid>,
618 request_id: Option<u64>,
619 trade_index: Option<i64>,
620 action: Action,
621 payload: Option<Payload>,
622 ) -> Self {
623 Self {
624 version: PROTOCOL_VER,
625 request_id,
626 trade_index,
627 id,
628 action,
629 payload,
630 }
631 }
632 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
634 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
635 }
636 pub fn as_json(&self) -> Result<String, ServiceError> {
638 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
639 }
640
641 pub fn get_action(&self) -> Action {
643 self.action.clone()
644 }
645
646 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
653 match &self.payload {
654 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
655 None => Ok(None),
656 _ => Err(ServiceError::InvalidPayload),
657 }
658 }
659
660 pub fn get_rating(&self) -> Result<u8, ServiceError> {
668 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
669 if !(MIN_RATING..=MAX_RATING).contains(&v) {
670 return Err(ServiceError::InvalidRatingValue);
671 }
672 Ok(v)
673 } else {
674 Err(ServiceError::InvalidRating)
675 }
676 }
677
678 pub fn verify(&self) -> bool {
685 match &self.action {
686 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
687 Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
688 if self.id.is_none() {
689 return false;
690 }
691 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
692 }
693 Action::AddBondInvoice => {
694 if self.id.is_none() {
695 return false;
696 }
697 matches!(
703 &self.payload,
704 Some(Payload::BondPayoutRequest(_)) | Some(Payload::PaymentRequest(_, _, _))
705 )
706 }
707 Action::AdminSettle | Action::AdminCancel => {
708 if self.id.is_none() {
709 return false;
710 }
711 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
712 }
713 Action::TakeSell
714 | Action::TakeBuy
715 | Action::FiatSent
716 | Action::FiatSentOk
717 | Action::Release
718 | Action::Released
719 | Action::Dispute
720 | Action::AdminCanceled
721 | Action::AdminSettled
722 | Action::Rate
723 | Action::RateReceived
724 | Action::AdminTakeDispute
725 | Action::AdminTookDispute
726 | Action::DisputeInitiatedByYou
727 | Action::DisputeInitiatedByPeer
728 | Action::WaitingBuyerInvoice
729 | Action::PurchaseCompleted
730 | Action::BondPayoutCompleted
731 | Action::HoldInvoicePaymentAccepted
732 | Action::HoldInvoicePaymentSettled
733 | Action::HoldInvoicePaymentCanceled
734 | Action::WaitingSellerToPay
735 | Action::BuyerTookOrder
736 | Action::BuyerInvoiceAccepted
737 | Action::BondInvoiceAccepted
738 | Action::CooperativeCancelInitiatedByYou
739 | Action::CooperativeCancelInitiatedByPeer
740 | Action::CooperativeCancelAccepted
741 | Action::Cancel
742 | Action::InvoiceUpdated
743 | Action::AdminAddSolver
744 | Action::SendDm
745 | Action::TradePubkey
746 | Action::Canceled => {
747 if self.id.is_none() {
748 return false;
749 }
750 !matches!(
751 &self.payload,
752 Some(Payload::BondResolution(_)) | Some(Payload::BondPayoutRequest(_))
753 )
754 }
755 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
756 Action::PaymentFailed => {
757 if self.id.is_none() {
758 return false;
759 }
760 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
761 }
762 Action::RateUser => {
763 matches!(&self.payload, Some(Payload::RatingUser(_)))
764 }
765 Action::CantDo => {
766 matches!(&self.payload, Some(Payload::CantDo(_)))
767 }
768 Action::Orders => {
769 matches!(
770 &self.payload,
771 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
772 )
773 }
774 }
775 }
776
777 pub fn get_order(&self) -> Option<&SmallOrder> {
782 if self.action != Action::NewOrder {
783 return None;
784 }
785 match &self.payload {
786 Some(Payload::Order(o)) => Some(o),
787 _ => None,
788 }
789 }
790
791 pub fn get_payment_request(&self) -> Option<String> {
798 if self.action != Action::TakeSell
799 && self.action != Action::AddInvoice
800 && self.action != Action::AddBondInvoice
801 && self.action != Action::NewOrder
802 {
803 return None;
804 }
805 match &self.payload {
806 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
807 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
808 _ => None,
809 }
810 }
811
812 pub fn get_amount(&self) -> Option<Amount> {
816 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
817 return None;
818 }
819 match &self.payload {
820 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
821 Some(Payload::Amount(amount)) => Some(*amount),
822 _ => None,
823 }
824 }
825
826 pub fn get_payload(&self) -> Option<&Payload> {
828 self.payload.as_ref()
829 }
830
831 pub fn has_trade_index(&self) -> (bool, i64) {
834 if let Some(index) = self.trade_index {
835 return (true, index);
836 }
837 (false, 0)
838 }
839
840 pub fn trade_index(&self) -> i64 {
842 if let Some(index) = self.trade_index {
843 return index;
844 }
845 0
846 }
847}
848
849#[cfg(test)]
850mod test {
851 use crate::message::{Action, BondPayoutRequest, Message, MessageKind, Payload, Peer};
852 use crate::order::SmallOrder;
853 use crate::user::UserInfo;
854 use nostr_sdk::Keys;
855 use uuid::uuid;
856
857 #[test]
858 fn test_peer_with_reputation() {
859 let reputation = UserInfo {
861 rating: 4.5,
862 reviews: 10,
863 operating_days: 30,
864 };
865 let peer = Peer::new(
866 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
867 Some(reputation.clone()),
868 );
869
870 assert_eq!(
872 peer.pubkey,
873 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
874 );
875 assert!(peer.reputation.is_some());
876 let peer_reputation = peer.reputation.clone().unwrap();
877 assert_eq!(peer_reputation.rating, 4.5);
878 assert_eq!(peer_reputation.reviews, 10);
879 assert_eq!(peer_reputation.operating_days, 30);
880
881 let json = peer.as_json().unwrap();
883 let deserialized_peer = Peer::from_json(&json).unwrap();
884 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
885 assert!(deserialized_peer.reputation.is_some());
886 let deserialized_reputation = deserialized_peer.reputation.unwrap();
887 assert_eq!(deserialized_reputation.rating, 4.5);
888 assert_eq!(deserialized_reputation.reviews, 10);
889 assert_eq!(deserialized_reputation.operating_days, 30);
890 }
891
892 #[test]
893 fn test_peer_without_reputation() {
894 let peer = Peer::new(
896 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
897 None,
898 );
899
900 assert_eq!(
902 peer.pubkey,
903 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
904 );
905 assert!(peer.reputation.is_none());
906
907 let json = peer.as_json().unwrap();
909 let deserialized_peer = Peer::from_json(&json).unwrap();
910 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
911 assert!(deserialized_peer.reputation.is_none());
912 }
913
914 #[test]
915 fn test_peer_in_message() {
916 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
917
918 let reputation = UserInfo {
920 rating: 4.5,
921 reviews: 10,
922 operating_days: 30,
923 };
924 let peer_with_reputation = Peer::new(
925 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
926 Some(reputation),
927 );
928 let payload_with_reputation = Payload::Peer(peer_with_reputation);
929 let message_with_reputation = Message::Order(MessageKind::new(
930 Some(uuid),
931 Some(1),
932 Some(2),
933 Action::FiatSentOk,
934 Some(payload_with_reputation),
935 ));
936
937 assert!(message_with_reputation.verify());
939 let message_json = message_with_reputation.as_json().unwrap();
940 let deserialized_message = Message::from_json(&message_json).unwrap();
941 assert!(deserialized_message.verify());
942
943 let peer_without_reputation = Peer::new(
945 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
946 None,
947 );
948 let payload_without_reputation = Payload::Peer(peer_without_reputation);
949 let message_without_reputation = Message::Order(MessageKind::new(
950 Some(uuid),
951 Some(1),
952 Some(2),
953 Action::FiatSentOk,
954 Some(payload_without_reputation),
955 ));
956
957 assert!(message_without_reputation.verify());
959 let message_json = message_without_reputation.as_json().unwrap();
960 let deserialized_message = Message::from_json(&message_json).unwrap();
961 assert!(deserialized_message.verify());
962 }
963
964 #[test]
965 fn test_bond_payout_request_payload_verifies_on_add_bond_invoice() {
966 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
967 let order = SmallOrder {
968 id: Some(order_id),
969 kind: None,
970 status: None,
971 amount: 500,
972 fiat_code: "USD".to_string(),
973 min_amount: None,
974 max_amount: None,
975 fiat_amount: 0,
976 payment_method: "lightning".to_string(),
977 premium: 0,
978 buyer_trade_pubkey: None,
979 seller_trade_pubkey: None,
980 buyer_invoice: None,
981 created_at: None,
982 expires_at: None,
983 };
984 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
985 order,
986 slashed_at: 1_734_000_000,
987 });
988 let kind = MessageKind::new(
989 Some(order_id),
990 None,
991 None,
992 Action::AddBondInvoice,
993 Some(payload),
994 );
995 assert!(
996 kind.verify(),
997 "BondPayoutRequest must verify on AddBondInvoice"
998 );
999
1000 let m = Message::Order(kind);
1003 let json = m.as_json().unwrap();
1004 assert!(json.contains("bond_payout_request"));
1005 let back = Message::from_json(&json).unwrap();
1006 assert!(back.verify());
1007 }
1008
1009 #[test]
1010 fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1011 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1014 let order = SmallOrder {
1015 id: Some(order_id),
1016 kind: None,
1017 status: None,
1018 amount: 500,
1019 fiat_code: "USD".to_string(),
1020 min_amount: None,
1021 max_amount: None,
1022 fiat_amount: 0,
1023 payment_method: "lightning".to_string(),
1024 premium: 0,
1025 buyer_trade_pubkey: None,
1026 seller_trade_pubkey: None,
1027 buyer_invoice: None,
1028 created_at: None,
1029 expires_at: None,
1030 };
1031
1032 let _exhaustive: fn(Action) = |a| match a {
1037 Action::AddBondInvoice => {}
1038 Action::NewOrder
1039 | Action::TakeSell
1040 | Action::TakeBuy
1041 | Action::PayInvoice
1042 | Action::PayBondInvoice
1043 | Action::FiatSent
1044 | Action::FiatSentOk
1045 | Action::Release
1046 | Action::Released
1047 | Action::Cancel
1048 | Action::Canceled
1049 | Action::CooperativeCancelInitiatedByYou
1050 | Action::CooperativeCancelInitiatedByPeer
1051 | Action::DisputeInitiatedByYou
1052 | Action::DisputeInitiatedByPeer
1053 | Action::CooperativeCancelAccepted
1054 | Action::BuyerInvoiceAccepted
1055 | Action::BondInvoiceAccepted
1056 | Action::PurchaseCompleted
1057 | Action::BondPayoutCompleted
1058 | Action::HoldInvoicePaymentAccepted
1059 | Action::HoldInvoicePaymentSettled
1060 | Action::HoldInvoicePaymentCanceled
1061 | Action::WaitingSellerToPay
1062 | Action::WaitingBuyerInvoice
1063 | Action::AddInvoice
1064 | Action::BuyerTookOrder
1065 | Action::Rate
1066 | Action::RateUser
1067 | Action::RateReceived
1068 | Action::CantDo
1069 | Action::Dispute
1070 | Action::AdminCancel
1071 | Action::AdminCanceled
1072 | Action::AdminSettle
1073 | Action::AdminSettled
1074 | Action::AdminAddSolver
1075 | Action::AdminTakeDispute
1076 | Action::AdminTookDispute
1077 | Action::PaymentFailed
1078 | Action::InvoiceUpdated
1079 | Action::SendDm
1080 | Action::TradePubkey
1081 | Action::RestoreSession
1082 | Action::LastTradeIndex
1083 | Action::Orders => {}
1084 };
1085
1086 let other_actions: &[Action] = &[
1087 Action::NewOrder,
1088 Action::TakeSell,
1089 Action::TakeBuy,
1090 Action::PayInvoice,
1091 Action::PayBondInvoice,
1092 Action::FiatSent,
1093 Action::FiatSentOk,
1094 Action::Release,
1095 Action::Released,
1096 Action::Cancel,
1097 Action::Canceled,
1098 Action::CooperativeCancelInitiatedByYou,
1099 Action::CooperativeCancelInitiatedByPeer,
1100 Action::DisputeInitiatedByYou,
1101 Action::DisputeInitiatedByPeer,
1102 Action::CooperativeCancelAccepted,
1103 Action::BuyerInvoiceAccepted,
1104 Action::BondInvoiceAccepted,
1105 Action::PurchaseCompleted,
1106 Action::BondPayoutCompleted,
1107 Action::HoldInvoicePaymentAccepted,
1108 Action::HoldInvoicePaymentSettled,
1109 Action::HoldInvoicePaymentCanceled,
1110 Action::WaitingSellerToPay,
1111 Action::WaitingBuyerInvoice,
1112 Action::AddInvoice,
1113 Action::BuyerTookOrder,
1114 Action::Rate,
1115 Action::RateUser,
1116 Action::RateReceived,
1117 Action::CantDo,
1118 Action::Dispute,
1119 Action::AdminCancel,
1120 Action::AdminCanceled,
1121 Action::AdminSettle,
1122 Action::AdminSettled,
1123 Action::AdminAddSolver,
1124 Action::AdminTakeDispute,
1125 Action::AdminTookDispute,
1126 Action::PaymentFailed,
1127 Action::InvoiceUpdated,
1128 Action::SendDm,
1129 Action::TradePubkey,
1130 Action::RestoreSession,
1131 Action::LastTradeIndex,
1132 Action::Orders,
1133 ];
1134
1135 for action in other_actions {
1136 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1137 order: order.clone(),
1138 slashed_at: 0,
1139 });
1140 let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1141 assert!(
1142 !kind.verify(),
1143 "BondPayoutRequest must be rejected on {action:?}"
1144 );
1145 }
1146 }
1147
1148 #[test]
1149 fn test_bond_payout_ack_actions_verify_and_wire_format() {
1150 use crate::message::BondResolution;
1151
1152 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1153
1154 let order = || SmallOrder {
1157 id: Some(order_id),
1158 kind: None,
1159 status: None,
1160 amount: 500,
1161 fiat_code: "USD".to_string(),
1162 min_amount: None,
1163 max_amount: None,
1164 fiat_amount: 0,
1165 payment_method: "lightning".to_string(),
1166 premium: 0,
1167 buyer_trade_pubkey: None,
1168 seller_trade_pubkey: None,
1169 buyer_invoice: None,
1170 created_at: None,
1171 expires_at: None,
1172 };
1173
1174 for (action, discriminator) in [
1178 (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1179 (Action::BondPayoutCompleted, "bond-payout-completed"),
1180 ] {
1181 let ok = Message::Order(MessageKind::new(
1183 Some(order_id),
1184 Some(1),
1185 Some(2),
1186 action.clone(),
1187 Some(Payload::Order(order())),
1188 ));
1189 assert!(ok.verify(), "{action:?} + Order should verify");
1190
1191 let no_id = Message::Order(MessageKind::new(
1193 None,
1194 Some(1),
1195 Some(2),
1196 action.clone(),
1197 Some(Payload::Order(order())),
1198 ));
1199 assert!(!no_id.verify(), "{action:?} without id must be rejected");
1200
1201 let with_resolution = Message::Order(MessageKind::new(
1203 Some(order_id),
1204 Some(1),
1205 Some(2),
1206 action.clone(),
1207 Some(Payload::BondResolution(BondResolution {
1208 slash_seller: true,
1209 slash_buyer: false,
1210 })),
1211 ));
1212 assert!(
1213 !with_resolution.verify(),
1214 "{action:?} + BondResolution must be rejected"
1215 );
1216
1217 let with_request = Message::Order(MessageKind::new(
1219 Some(order_id),
1220 Some(1),
1221 Some(2),
1222 action.clone(),
1223 Some(Payload::BondPayoutRequest(BondPayoutRequest {
1224 order: order(),
1225 slashed_at: 0,
1226 })),
1227 ));
1228 assert!(
1229 !with_request.verify(),
1230 "{action:?} + BondPayoutRequest must be rejected"
1231 );
1232
1233 let json = ok.as_json().unwrap();
1235 assert!(
1236 json.contains(&format!("\"action\":\"{discriminator}\"")),
1237 "expected kebab-case discriminator {discriminator}, got: {json}"
1238 );
1239 let decoded = Message::from_json(&json).unwrap();
1240 assert!(decoded.verify());
1241 assert_eq!(decoded.inner_action(), Some(action));
1242 }
1243 }
1244
1245 #[test]
1246 fn test_payment_failed_payload() {
1247 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1248
1249 let payment_failed_info = crate::message::PaymentFailedInfo {
1251 payment_attempts: 3,
1252 payment_retries_interval: 60,
1253 };
1254
1255 let payload = Payload::PaymentFailed(payment_failed_info);
1256 let message = Message::Order(MessageKind::new(
1257 Some(uuid),
1258 Some(1),
1259 Some(2),
1260 Action::PaymentFailed,
1261 Some(payload),
1262 ));
1263
1264 assert!(message.verify());
1266
1267 let message_json = message.as_json().unwrap();
1269
1270 let deserialized_message = Message::from_json(&message_json).unwrap();
1272 assert!(deserialized_message.verify());
1273
1274 if let Message::Order(kind) = deserialized_message {
1276 if let Some(Payload::PaymentFailed(info)) = kind.payload {
1277 assert_eq!(info.payment_attempts, 3);
1278 assert_eq!(info.payment_retries_interval, 60);
1279 } else {
1280 panic!("Expected PaymentFailed payload");
1281 }
1282 } else {
1283 panic!("Expected Order message");
1284 }
1285 }
1286
1287 #[test]
1288 fn test_message_payload_signature() {
1289 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1290 let peer = Peer::new(
1291 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1292 None, );
1294 let payload = Payload::Peer(peer);
1295 let test_message = Message::Order(MessageKind::new(
1296 Some(uuid),
1297 Some(1),
1298 Some(2),
1299 Action::FiatSentOk,
1300 Some(payload),
1301 ));
1302 assert!(test_message.verify());
1303 let test_message_json = test_message.as_json().unwrap();
1304 let trade_keys =
1306 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1307 .unwrap();
1308 let sig = Message::sign(test_message_json.clone(), &trade_keys);
1309
1310 assert!(Message::verify_signature(
1311 test_message_json,
1312 trade_keys.public_key(),
1313 sig
1314 ));
1315 }
1316
1317 #[test]
1318 fn test_restore_session_message() {
1319 let restore_request_message = Message::Restore(MessageKind::new(
1321 None,
1322 None,
1323 None,
1324 Action::RestoreSession,
1325 None,
1326 ));
1327
1328 assert!(restore_request_message.verify());
1330 assert_eq!(
1331 restore_request_message.inner_action(),
1332 Some(Action::RestoreSession)
1333 );
1334
1335 let message_json = restore_request_message.as_json().unwrap();
1337 let deserialized_message = Message::from_json(&message_json).unwrap();
1338 assert!(deserialized_message.verify());
1339 assert_eq!(
1340 deserialized_message.inner_action(),
1341 Some(Action::RestoreSession)
1342 );
1343
1344 let restored_orders = vec![
1346 crate::message::RestoredOrdersInfo {
1347 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1348 trade_index: 1,
1349 status: "active".to_string(),
1350 },
1351 crate::message::RestoredOrdersInfo {
1352 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1353 trade_index: 2,
1354 status: "success".to_string(),
1355 },
1356 ];
1357
1358 let restored_disputes = vec![
1359 crate::message::RestoredDisputesInfo {
1360 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1361 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1362 trade_index: 1,
1363 status: "initiated".to_string(),
1364 initiator: Some(crate::message::DisputeInitiator::Buyer),
1365 solver_pubkey: None,
1366 },
1367 crate::message::RestoredDisputesInfo {
1368 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1369 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1370 trade_index: 2,
1371 status: "in-progress".to_string(),
1372 initiator: None,
1373 solver_pubkey: Some(
1374 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1375 ),
1376 },
1377 crate::message::RestoredDisputesInfo {
1378 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1379 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1380 trade_index: 3,
1381 status: "initiated".to_string(),
1382 initiator: Some(crate::message::DisputeInitiator::Seller),
1383 solver_pubkey: None,
1384 },
1385 ];
1386
1387 let restore_session_info = crate::message::RestoreSessionInfo {
1388 restore_orders: restored_orders.clone(),
1389 restore_disputes: restored_disputes.clone(),
1390 };
1391
1392 let restore_data_payload = Payload::RestoreData(restore_session_info);
1393 let restore_data_message = Message::Restore(MessageKind::new(
1394 None,
1395 None,
1396 None,
1397 Action::RestoreSession,
1398 Some(restore_data_payload),
1399 ));
1400
1401 assert!(!restore_data_message.verify());
1403
1404 let message_json = restore_data_message.as_json().unwrap();
1406 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1407
1408 if let Message::Restore(kind) = deserialized_restore_message {
1409 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1410 assert_eq!(session_info.restore_disputes.len(), 3);
1411 assert_eq!(
1412 session_info.restore_disputes[0].initiator,
1413 Some(crate::message::DisputeInitiator::Buyer)
1414 );
1415 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1416 assert_eq!(session_info.restore_disputes[1].initiator, None);
1417 assert_eq!(
1418 session_info.restore_disputes[1].solver_pubkey,
1419 Some(
1420 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1421 .to_string()
1422 )
1423 );
1424 assert_eq!(
1425 session_info.restore_disputes[2].initiator,
1426 Some(crate::message::DisputeInitiator::Seller)
1427 );
1428 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1429 } else {
1430 panic!("Expected RestoreData payload");
1431 }
1432 } else {
1433 panic!("Expected Restore message");
1434 }
1435 }
1436
1437 #[test]
1438 fn test_restore_session_message_validation() {
1439 let restore_request_message = Message::Restore(MessageKind::new(
1441 None,
1442 None,
1443 None,
1444 Action::RestoreSession,
1445 None, ));
1447
1448 assert!(restore_request_message.verify());
1450
1451 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1453 let wrong_message = Message::Restore(MessageKind::new(
1454 None,
1455 None,
1456 None,
1457 Action::RestoreSession,
1458 Some(wrong_payload),
1459 ));
1460
1461 assert!(!wrong_message.verify());
1463
1464 let with_id = Message::Restore(MessageKind::new(
1466 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1467 None,
1468 None,
1469 Action::RestoreSession,
1470 None,
1471 ));
1472 assert!(with_id.verify());
1473
1474 let with_request_id = Message::Restore(MessageKind::new(
1475 None,
1476 Some(42),
1477 None,
1478 Action::RestoreSession,
1479 None,
1480 ));
1481 assert!(with_request_id.verify());
1482
1483 let with_trade_index = Message::Restore(MessageKind::new(
1484 None,
1485 None,
1486 Some(7),
1487 Action::RestoreSession,
1488 None,
1489 ));
1490 assert!(with_trade_index.verify());
1491 }
1492
1493 #[test]
1494 fn test_restore_session_message_constructor() {
1495 let restore_request_message = Message::new_restore(None);
1497
1498 assert!(matches!(restore_request_message, Message::Restore(_)));
1499 assert!(restore_request_message.verify());
1500 assert_eq!(
1501 restore_request_message.inner_action(),
1502 Some(Action::RestoreSession)
1503 );
1504
1505 let restore_session_info = crate::message::RestoreSessionInfo {
1507 restore_orders: vec![],
1508 restore_disputes: vec![],
1509 };
1510 let restore_data_message =
1511 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1512
1513 assert!(matches!(restore_data_message, Message::Restore(_)));
1514 assert!(!restore_data_message.verify());
1515 }
1516
1517 #[test]
1518 fn test_last_trade_index_valid_message() {
1519 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1520 let msg = Message::Restore(kind);
1521
1522 assert!(msg.verify());
1523
1524 let json = msg.as_json().unwrap();
1526 let decoded = Message::from_json(&json).unwrap();
1527 assert!(decoded.verify());
1528
1529 let inner = decoded.get_inner_message_kind();
1531 assert_eq!(inner.trade_index(), 7);
1532 assert_eq!(inner.has_trade_index(), (true, 7));
1533 }
1534
1535 #[test]
1536 fn test_last_trade_index_without_id_is_valid() {
1537 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1539 let msg = Message::Restore(kind);
1540 assert!(msg.verify());
1541 }
1542
1543 #[test]
1544 fn test_last_trade_index_with_payload_fails_validation() {
1545 let kind = MessageKind::new(
1547 None,
1548 None,
1549 Some(3),
1550 Action::LastTradeIndex,
1551 Some(Payload::TextMessage("ignored".to_string())),
1552 );
1553 let msg = Message::Restore(kind);
1554 assert!(!msg.verify());
1555 }
1556
1557 #[test]
1558 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1559 use crate::message::BondResolution;
1560
1561 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1562
1563 for action in [Action::AdminSettle, Action::AdminCancel] {
1564 let with_resolution = Message::Order(MessageKind::new(
1565 Some(uuid),
1566 Some(1),
1567 Some(2),
1568 action.clone(),
1569 Some(Payload::BondResolution(BondResolution {
1570 slash_seller: true,
1571 slash_buyer: false,
1572 })),
1573 ));
1574 assert!(
1575 with_resolution.verify(),
1576 "{action:?} + BondResolution should verify"
1577 );
1578
1579 let without_payload = Message::Order(MessageKind::new(
1580 Some(uuid),
1581 Some(1),
1582 Some(2),
1583 action.clone(),
1584 None,
1585 ));
1586 assert!(without_payload.verify(), "{action:?} + None should verify");
1587
1588 let wrong = Message::Order(MessageKind::new(
1590 Some(uuid),
1591 Some(1),
1592 Some(2),
1593 action.clone(),
1594 Some(Payload::TextMessage("nope".to_string())),
1595 ));
1596 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1597
1598 let no_id = Message::Order(MessageKind::new(
1600 None,
1601 Some(1),
1602 Some(2),
1603 action,
1604 Some(Payload::BondResolution(BondResolution {
1605 slash_seller: false,
1606 slash_buyer: false,
1607 })),
1608 ));
1609 assert!(!no_id.verify(), "admin action without id must be rejected");
1610 }
1611 }
1612
1613 #[test]
1614 fn test_bond_resolution_rejected_on_non_admin_actions() {
1615 use crate::message::BondResolution;
1616
1617 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1618 let payload = Payload::BondResolution(BondResolution {
1619 slash_seller: true,
1620 slash_buyer: true,
1621 });
1622
1623 for action in [
1627 Action::NewOrder,
1628 Action::TakeSell,
1629 Action::TakeBuy,
1630 Action::PayInvoice,
1631 Action::PayBondInvoice,
1632 Action::FiatSent,
1633 Action::FiatSentOk,
1634 Action::Release,
1635 Action::Released,
1636 Action::Cancel,
1637 Action::Canceled,
1638 Action::CooperativeCancelInitiatedByYou,
1639 Action::CooperativeCancelInitiatedByPeer,
1640 Action::DisputeInitiatedByYou,
1641 Action::DisputeInitiatedByPeer,
1642 Action::CooperativeCancelAccepted,
1643 Action::BuyerInvoiceAccepted,
1644 Action::BondInvoiceAccepted,
1645 Action::PurchaseCompleted,
1646 Action::BondPayoutCompleted,
1647 Action::HoldInvoicePaymentAccepted,
1648 Action::HoldInvoicePaymentSettled,
1649 Action::HoldInvoicePaymentCanceled,
1650 Action::WaitingSellerToPay,
1651 Action::WaitingBuyerInvoice,
1652 Action::AddInvoice,
1653 Action::AddBondInvoice,
1654 Action::BuyerTookOrder,
1655 Action::Rate,
1656 Action::RateUser,
1657 Action::RateReceived,
1658 Action::CantDo,
1659 Action::Dispute,
1660 Action::AdminCanceled,
1661 Action::AdminSettled,
1662 Action::AdminAddSolver,
1663 Action::AdminTakeDispute,
1664 Action::AdminTookDispute,
1665 Action::PaymentFailed,
1666 Action::InvoiceUpdated,
1667 Action::SendDm,
1668 Action::TradePubkey,
1669 Action::RestoreSession,
1670 Action::LastTradeIndex,
1671 Action::Orders,
1672 ] {
1673 let msg = Message::Order(MessageKind::new(
1674 Some(uuid),
1675 Some(1),
1676 Some(2),
1677 action.clone(),
1678 Some(payload.clone()),
1679 ));
1680 assert!(
1681 !msg.verify(),
1682 "{action:?} must reject BondResolution payload"
1683 );
1684 }
1685 }
1686
1687 #[test]
1688 fn test_bond_resolution_wire_format() {
1689 use crate::message::BondResolution;
1690
1691 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1692 let msg = Message::Order(MessageKind::new(
1693 Some(uuid),
1694 None,
1695 None,
1696 Action::AdminCancel,
1697 Some(Payload::BondResolution(BondResolution {
1698 slash_seller: true,
1699 slash_buyer: false,
1700 })),
1701 ));
1702
1703 let json = msg.as_json().unwrap();
1704 assert!(
1706 json.contains("\"bond_resolution\""),
1707 "expected snake_case discriminator, got: {json}"
1708 );
1709 assert!(json.contains("\"slash_seller\":true"));
1710 assert!(json.contains("\"slash_buyer\":false"));
1711
1712 let decoded = Message::from_json(&json).unwrap();
1714 assert!(decoded.verify());
1715 if let Message::Order(kind) = decoded {
1716 match kind.payload {
1717 Some(Payload::BondResolution(b)) => {
1718 assert!(b.slash_seller);
1719 assert!(!b.slash_buyer);
1720 }
1721 other => panic!("expected BondResolution payload, got {other:?}"),
1722 }
1723 } else {
1724 panic!("expected Order message");
1725 }
1726 }
1727
1728 #[test]
1729 fn test_bond_resolution_legacy_null_payload() {
1730 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1734 let json = format!(
1735 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
1736 );
1737 let msg = Message::from_json(&json).unwrap();
1738 assert!(msg.verify());
1739 }
1740
1741 #[test]
1742 fn test_pay_bond_invoice_wire_format_and_verify() {
1743 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1744 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1745
1746 let msg = Message::Order(MessageKind::new(
1747 Some(uuid),
1748 Some(1),
1749 Some(2),
1750 Action::PayBondInvoice,
1751 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1752 ));
1753 assert!(msg.verify());
1754
1755 let json = msg.as_json().unwrap();
1757 assert!(
1758 json.contains("\"action\":\"pay-bond-invoice\""),
1759 "expected kebab-case discriminator, got: {json}"
1760 );
1761
1762 let decoded = Message::from_json(&json).unwrap();
1764 assert!(decoded.verify());
1765 assert!(matches!(
1766 decoded.inner_action(),
1767 Some(Action::PayBondInvoice)
1768 ));
1769
1770 let no_id = Message::Order(MessageKind::new(
1772 None,
1773 Some(1),
1774 Some(2),
1775 Action::PayBondInvoice,
1776 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1777 ));
1778 assert!(!no_id.verify());
1779
1780 let wrong_payload = Message::Order(MessageKind::new(
1782 Some(uuid),
1783 Some(1),
1784 Some(2),
1785 Action::PayBondInvoice,
1786 Some(Payload::TextMessage("nope".to_string())),
1787 ));
1788 assert!(!wrong_payload.verify());
1789
1790 let no_payload = Message::Order(MessageKind::new(
1792 Some(uuid),
1793 Some(1),
1794 Some(2),
1795 Action::PayBondInvoice,
1796 None,
1797 ));
1798 assert!(!no_payload.verify());
1799 }
1800
1801 #[test]
1802 fn test_add_bond_invoice_wire_format_and_verify() {
1803 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1804 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1805
1806 let msg = Message::Order(MessageKind::new(
1807 Some(uuid),
1808 Some(1),
1809 Some(2),
1810 Action::AddBondInvoice,
1811 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1812 ));
1813 assert!(msg.verify());
1814
1815 let json = msg.as_json().unwrap();
1817 assert!(
1818 json.contains("\"action\":\"add-bond-invoice\""),
1819 "expected kebab-case discriminator, got: {json}"
1820 );
1821
1822 let decoded = Message::from_json(&json).unwrap();
1824 assert!(decoded.verify());
1825 assert!(matches!(
1826 decoded.inner_action(),
1827 Some(Action::AddBondInvoice)
1828 ));
1829
1830 let no_id = Message::Order(MessageKind::new(
1832 None,
1833 Some(1),
1834 Some(2),
1835 Action::AddBondInvoice,
1836 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1837 ));
1838 assert!(!no_id.verify());
1839
1840 let wrong_payload = Message::Order(MessageKind::new(
1842 Some(uuid),
1843 Some(1),
1844 Some(2),
1845 Action::AddBondInvoice,
1846 Some(Payload::TextMessage("nope".to_string())),
1847 ));
1848 assert!(!wrong_payload.verify());
1849
1850 let no_payload = Message::Order(MessageKind::new(
1852 Some(uuid),
1853 Some(1),
1854 Some(2),
1855 Action::AddBondInvoice,
1856 None,
1857 ));
1858 assert!(!no_payload.verify());
1859
1860 if let Message::Order(kind) = &msg {
1862 assert_eq!(kind.get_payment_request(), Some(bolt11));
1863 } else {
1864 panic!("expected Message::Order");
1865 }
1866 }
1867
1868 #[test]
1869 fn test_restored_dispute_helper_serialization_roundtrip() {
1870 use crate::message::RestoredDisputeHelper;
1871
1872 let helper = RestoredDisputeHelper {
1873 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1874 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1875 dispute_status: "initiated".to_string(),
1876 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
1877 master_seller_pubkey: Some("npub1sellerkey".to_string()),
1878 trade_index_buyer: Some(1),
1879 trade_index_seller: Some(2),
1880 buyer_dispute: true,
1881 seller_dispute: false,
1882 solver_pubkey: None,
1883 };
1884
1885 let json = serde_json::to_string(&helper).unwrap();
1886 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
1887
1888 assert_eq!(deserialized.dispute_id, helper.dispute_id);
1889 assert_eq!(deserialized.order_id, helper.order_id);
1890 assert_eq!(deserialized.dispute_status, helper.dispute_status);
1891 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
1892 assert_eq!(
1893 deserialized.master_seller_pubkey,
1894 helper.master_seller_pubkey
1895 );
1896 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
1897 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
1898 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
1899 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
1900 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
1901
1902 let helper_seller_dispute = RestoredDisputeHelper {
1903 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1904 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1905 dispute_status: "in-progress".to_string(),
1906 master_buyer_pubkey: None,
1907 master_seller_pubkey: None,
1908 trade_index_buyer: None,
1909 trade_index_seller: None,
1910 buyer_dispute: false,
1911 seller_dispute: true,
1912 solver_pubkey: Some(
1913 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1914 ),
1915 };
1916
1917 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
1918 let deserialized_seller: RestoredDisputeHelper =
1919 serde_json::from_str(&json_seller).unwrap();
1920
1921 assert_eq!(
1922 deserialized_seller.dispute_id,
1923 helper_seller_dispute.dispute_id
1924 );
1925 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
1926 assert_eq!(
1927 deserialized_seller.dispute_status,
1928 helper_seller_dispute.dispute_status
1929 );
1930 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
1931 assert_eq!(deserialized_seller.master_seller_pubkey, None);
1932 assert_eq!(deserialized_seller.trade_index_buyer, None);
1933 assert_eq!(deserialized_seller.trade_index_seller, None);
1934 assert!(!deserialized_seller.buyer_dispute);
1935 assert!(deserialized_seller.seller_dispute);
1936 assert_eq!(
1937 deserialized_seller.solver_pubkey,
1938 helper_seller_dispute.solver_pubkey
1939 );
1940 }
1941}