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 BondSlashed,
130 HoldInvoicePaymentAccepted,
132 HoldInvoicePaymentSettled,
134 HoldInvoicePaymentCanceled,
136 WaitingSellerToPay,
138 WaitingBuyerInvoice,
140 AddInvoice,
143 AddBondInvoice,
150 BuyerTookOrder,
152 Rate,
154 RateUser,
156 RateReceived,
158 CantDo,
160 Dispute,
162 AdminCancel,
164 AdminCanceled,
166 AdminSettle,
168 AdminSettled,
170 AdminAddSolver,
172 AdminTakeDispute,
174 AdminTookDispute,
176 PaymentFailed,
179 InvoiceUpdated,
181 SendDm,
183 TradePubkey,
185 RestoreSession,
187 LastTradeIndex,
190 Orders,
193}
194
195impl fmt::Display for Action {
196 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
197 write!(f, "{self:?}")
198 }
199}
200
201#[derive(Debug, Clone, Deserialize, Serialize)]
208#[serde(rename_all = "kebab-case")]
209pub enum Message {
210 Order(MessageKind),
212 Dispute(MessageKind),
214 CantDo(MessageKind),
216 Rate(MessageKind),
218 Dm(MessageKind),
220 Restore(MessageKind),
222}
223
224impl Message {
225 pub fn new_order(
228 id: Option<Uuid>,
229 request_id: Option<u64>,
230 trade_index: Option<i64>,
231 action: Action,
232 payload: Option<Payload>,
233 ) -> Self {
234 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
235 Self::Order(kind)
236 }
237
238 pub fn new_dispute(
241 id: Option<Uuid>,
242 request_id: Option<u64>,
243 trade_index: Option<i64>,
244 action: Action,
245 payload: Option<Payload>,
246 ) -> Self {
247 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
248
249 Self::Dispute(kind)
250 }
251
252 pub fn new_restore(payload: Option<Payload>) -> Self {
257 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
258 Self::Restore(kind)
259 }
260
261 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
264 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
265
266 Self::CantDo(kind)
267 }
268
269 pub fn new_dm(
271 id: Option<Uuid>,
272 request_id: Option<u64>,
273 action: Action,
274 payload: Option<Payload>,
275 ) -> Self {
276 let kind = MessageKind::new(id, request_id, None, action, payload);
277
278 Self::Dm(kind)
279 }
280
281 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
283 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
284 }
285
286 pub fn as_json(&self) -> Result<String, ServiceError> {
288 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
289 }
290
291 pub fn get_inner_message_kind(&self) -> &MessageKind {
293 match self {
294 Message::Dispute(k)
295 | Message::Order(k)
296 | Message::CantDo(k)
297 | Message::Rate(k)
298 | Message::Dm(k)
299 | Message::Restore(k) => k,
300 }
301 }
302
303 pub fn inner_action(&self) -> Option<Action> {
308 match self {
309 Message::Dispute(a)
310 | Message::Order(a)
311 | Message::CantDo(a)
312 | Message::Rate(a)
313 | Message::Dm(a)
314 | Message::Restore(a) => Some(a.get_action()),
315 }
316 }
317
318 pub fn verify(&self) -> bool {
321 match self {
322 Message::Order(m)
323 | Message::Dispute(m)
324 | Message::CantDo(m)
325 | Message::Rate(m)
326 | Message::Dm(m)
327 | Message::Restore(m) => m.verify(),
328 }
329 }
330
331 pub fn sign(message: String, keys: &Keys) -> Signature {
340 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
341 let hash = hash.to_byte_array();
342 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
343
344 keys.sign_schnorr(&message)
345 }
346
347 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
353 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
355 let hash = hash.to_byte_array();
356 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
357
358 let secp = Secp256k1::verification_only();
360 if let Ok(xonlykey) = pubkey.xonly() {
362 xonlykey.verify(&secp, &message, &sig).is_ok()
363 } else {
364 false
365 }
366 }
367}
368
369#[derive(Debug, Clone, Deserialize, Serialize)]
376pub struct MessageKind {
377 pub version: u8,
380 pub request_id: Option<u64>,
383 pub trade_index: Option<i64>,
386 #[serde(skip_serializing_if = "Option::is_none")]
389 pub id: Option<Uuid>,
390 pub action: Action,
392 pub payload: Option<Payload>,
395}
396
397type Amount = i64;
399
400#[derive(Debug, Deserialize, Serialize, Clone)]
405pub struct PaymentFailedInfo {
406 pub payment_attempts: u32,
408 pub payment_retries_interval: u32,
410}
411
412#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
417#[derive(Debug, Deserialize, Serialize, Clone)]
418pub struct RestoredOrderHelper {
419 pub id: Uuid,
421 pub status: String,
423 pub master_buyer_pubkey: Option<String>,
425 pub master_seller_pubkey: Option<String>,
427 pub trade_index_buyer: Option<i64>,
429 pub trade_index_seller: Option<i64>,
431}
432
433#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
438#[derive(Debug, Deserialize, Serialize, Clone)]
439pub struct RestoredDisputeHelper {
440 pub dispute_id: Uuid,
442 pub order_id: Uuid,
444 pub dispute_status: String,
446 pub master_buyer_pubkey: Option<String>,
448 pub master_seller_pubkey: Option<String>,
450 pub trade_index_buyer: Option<i64>,
452 pub trade_index_seller: Option<i64>,
454 pub buyer_dispute: bool,
458 pub seller_dispute: bool,
462 pub solver_pubkey: Option<String>,
465}
466
467#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
469#[derive(Debug, Deserialize, Serialize, Clone)]
470pub struct RestoredOrdersInfo {
471 pub order_id: Uuid,
473 pub trade_index: i64,
475 pub status: String,
477}
478
479#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
481#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
482#[serde(rename_all = "lowercase")]
483#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
484pub enum DisputeInitiator {
485 Buyer,
487 Seller,
489}
490
491#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
493#[derive(Debug, Deserialize, Serialize, Clone)]
494pub struct RestoredDisputesInfo {
495 pub dispute_id: Uuid,
497 pub order_id: Uuid,
499 pub trade_index: i64,
501 pub status: String,
503 pub initiator: Option<DisputeInitiator>,
506 pub solver_pubkey: Option<String>,
509}
510
511#[derive(Debug, Deserialize, Serialize, Clone, Default)]
516pub struct RestoreSessionInfo {
517 #[serde(rename = "orders")]
519 pub restore_orders: Vec<RestoredOrdersInfo>,
520 #[serde(rename = "disputes")]
522 pub restore_disputes: Vec<RestoredDisputesInfo>,
523}
524
525#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
533pub struct BondResolution {
534 pub slash_seller: bool,
536 pub slash_buyer: bool,
538}
539
540#[derive(Debug, Deserialize, Serialize, Clone)]
555pub struct BondPayoutRequest {
556 pub order: SmallOrder,
560 pub slashed_at: i64,
565}
566
567#[derive(Debug, Deserialize, Serialize, Clone)]
573#[serde(rename_all = "snake_case")]
574pub enum Payload {
575 Order(SmallOrder),
577 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
585 TextMessage(String),
587 Peer(Peer),
589 RatingUser(u8),
591 Amount(Amount),
593 Dispute(Uuid, Option<SolverDisputeInfo>),
596 CantDo(Option<CantDoReason>),
598 NextTrade(String, u32),
601 PaymentFailed(PaymentFailedInfo),
603 RestoreData(RestoreSessionInfo),
605 Ids(Vec<Uuid>),
607 Orders(Vec<SmallOrder>),
609 BondResolution(BondResolution),
612 BondPayoutRequest(BondPayoutRequest),
618}
619
620#[allow(dead_code)]
621impl MessageKind {
622 pub fn new(
625 id: Option<Uuid>,
626 request_id: Option<u64>,
627 trade_index: Option<i64>,
628 action: Action,
629 payload: Option<Payload>,
630 ) -> Self {
631 Self {
632 version: PROTOCOL_VER,
633 request_id,
634 trade_index,
635 id,
636 action,
637 payload,
638 }
639 }
640 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
642 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
643 }
644 pub fn as_json(&self) -> Result<String, ServiceError> {
646 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
647 }
648
649 pub fn get_action(&self) -> Action {
651 self.action.clone()
652 }
653
654 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
661 match &self.payload {
662 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
663 None => Ok(None),
664 _ => Err(ServiceError::InvalidPayload),
665 }
666 }
667
668 pub fn get_rating(&self) -> Result<u8, ServiceError> {
676 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
677 if !(MIN_RATING..=MAX_RATING).contains(&v) {
678 return Err(ServiceError::InvalidRatingValue);
679 }
680 Ok(v)
681 } else {
682 Err(ServiceError::InvalidRating)
683 }
684 }
685
686 pub fn verify(&self) -> bool {
693 match &self.action {
694 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
695 Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
696 if self.id.is_none() {
697 return false;
698 }
699 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
700 }
701 Action::AddBondInvoice => {
702 if self.id.is_none() {
703 return false;
704 }
705 matches!(
711 &self.payload,
712 Some(Payload::BondPayoutRequest(_)) | Some(Payload::PaymentRequest(_, _, _))
713 )
714 }
715 Action::AdminSettle | Action::AdminCancel => {
716 if self.id.is_none() {
717 return false;
718 }
719 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
720 }
721 Action::TakeSell
722 | Action::TakeBuy
723 | Action::FiatSent
724 | Action::FiatSentOk
725 | Action::Release
726 | Action::Released
727 | Action::Dispute
728 | Action::AdminCanceled
729 | Action::AdminSettled
730 | Action::Rate
731 | Action::RateReceived
732 | Action::AdminTakeDispute
733 | Action::AdminTookDispute
734 | Action::DisputeInitiatedByYou
735 | Action::DisputeInitiatedByPeer
736 | Action::WaitingBuyerInvoice
737 | Action::PurchaseCompleted
738 | Action::BondPayoutCompleted
739 | Action::BondSlashed
740 | Action::HoldInvoicePaymentAccepted
741 | Action::HoldInvoicePaymentSettled
742 | Action::HoldInvoicePaymentCanceled
743 | Action::WaitingSellerToPay
744 | Action::BuyerTookOrder
745 | Action::BuyerInvoiceAccepted
746 | Action::BondInvoiceAccepted
747 | Action::CooperativeCancelInitiatedByYou
748 | Action::CooperativeCancelInitiatedByPeer
749 | Action::CooperativeCancelAccepted
750 | Action::Cancel
751 | Action::InvoiceUpdated
752 | Action::AdminAddSolver
753 | Action::SendDm
754 | Action::TradePubkey
755 | Action::Canceled => {
756 if self.id.is_none() {
757 return false;
758 }
759 !matches!(
760 &self.payload,
761 Some(Payload::BondResolution(_)) | Some(Payload::BondPayoutRequest(_))
762 )
763 }
764 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
765 Action::PaymentFailed => {
766 if self.id.is_none() {
767 return false;
768 }
769 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
770 }
771 Action::RateUser => {
772 matches!(&self.payload, Some(Payload::RatingUser(_)))
773 }
774 Action::CantDo => {
775 matches!(&self.payload, Some(Payload::CantDo(_)))
776 }
777 Action::Orders => {
778 matches!(
779 &self.payload,
780 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
781 )
782 }
783 }
784 }
785
786 pub fn get_order(&self) -> Option<&SmallOrder> {
791 if self.action != Action::NewOrder {
792 return None;
793 }
794 match &self.payload {
795 Some(Payload::Order(o)) => Some(o),
796 _ => None,
797 }
798 }
799
800 pub fn get_payment_request(&self) -> Option<String> {
807 if self.action != Action::TakeSell
808 && self.action != Action::AddInvoice
809 && self.action != Action::AddBondInvoice
810 && self.action != Action::NewOrder
811 {
812 return None;
813 }
814 match &self.payload {
815 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
816 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
817 _ => None,
818 }
819 }
820
821 pub fn get_amount(&self) -> Option<Amount> {
825 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
826 return None;
827 }
828 match &self.payload {
829 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
830 Some(Payload::Amount(amount)) => Some(*amount),
831 _ => None,
832 }
833 }
834
835 pub fn get_payload(&self) -> Option<&Payload> {
837 self.payload.as_ref()
838 }
839
840 pub fn has_trade_index(&self) -> (bool, i64) {
843 if let Some(index) = self.trade_index {
844 return (true, index);
845 }
846 (false, 0)
847 }
848
849 pub fn trade_index(&self) -> i64 {
851 if let Some(index) = self.trade_index {
852 return index;
853 }
854 0
855 }
856}
857
858#[cfg(test)]
859mod test {
860 use crate::message::{Action, BondPayoutRequest, Message, MessageKind, Payload, Peer};
861 use crate::order::SmallOrder;
862 use crate::user::UserInfo;
863 use nostr_sdk::Keys;
864 use uuid::uuid;
865
866 #[test]
867 fn test_peer_with_reputation() {
868 let reputation = UserInfo {
870 rating: 4.5,
871 reviews: 10,
872 operating_days: 30,
873 };
874 let peer = Peer::new(
875 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
876 Some(reputation.clone()),
877 );
878
879 assert_eq!(
881 peer.pubkey,
882 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
883 );
884 assert!(peer.reputation.is_some());
885 let peer_reputation = peer.reputation.clone().unwrap();
886 assert_eq!(peer_reputation.rating, 4.5);
887 assert_eq!(peer_reputation.reviews, 10);
888 assert_eq!(peer_reputation.operating_days, 30);
889
890 let json = peer.as_json().unwrap();
892 let deserialized_peer = Peer::from_json(&json).unwrap();
893 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
894 assert!(deserialized_peer.reputation.is_some());
895 let deserialized_reputation = deserialized_peer.reputation.unwrap();
896 assert_eq!(deserialized_reputation.rating, 4.5);
897 assert_eq!(deserialized_reputation.reviews, 10);
898 assert_eq!(deserialized_reputation.operating_days, 30);
899 }
900
901 #[test]
902 fn test_peer_without_reputation() {
903 let peer = Peer::new(
905 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
906 None,
907 );
908
909 assert_eq!(
911 peer.pubkey,
912 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
913 );
914 assert!(peer.reputation.is_none());
915
916 let json = peer.as_json().unwrap();
918 let deserialized_peer = Peer::from_json(&json).unwrap();
919 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
920 assert!(deserialized_peer.reputation.is_none());
921 }
922
923 #[test]
924 fn test_peer_in_message() {
925 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
926
927 let reputation = UserInfo {
929 rating: 4.5,
930 reviews: 10,
931 operating_days: 30,
932 };
933 let peer_with_reputation = Peer::new(
934 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
935 Some(reputation),
936 );
937 let payload_with_reputation = Payload::Peer(peer_with_reputation);
938 let message_with_reputation = Message::Order(MessageKind::new(
939 Some(uuid),
940 Some(1),
941 Some(2),
942 Action::FiatSentOk,
943 Some(payload_with_reputation),
944 ));
945
946 assert!(message_with_reputation.verify());
948 let message_json = message_with_reputation.as_json().unwrap();
949 let deserialized_message = Message::from_json(&message_json).unwrap();
950 assert!(deserialized_message.verify());
951
952 let peer_without_reputation = Peer::new(
954 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
955 None,
956 );
957 let payload_without_reputation = Payload::Peer(peer_without_reputation);
958 let message_without_reputation = Message::Order(MessageKind::new(
959 Some(uuid),
960 Some(1),
961 Some(2),
962 Action::FiatSentOk,
963 Some(payload_without_reputation),
964 ));
965
966 assert!(message_without_reputation.verify());
968 let message_json = message_without_reputation.as_json().unwrap();
969 let deserialized_message = Message::from_json(&message_json).unwrap();
970 assert!(deserialized_message.verify());
971 }
972
973 #[test]
974 fn test_bond_payout_request_payload_verifies_on_add_bond_invoice() {
975 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
976 let order = SmallOrder {
977 id: Some(order_id),
978 kind: None,
979 status: None,
980 amount: 500,
981 fiat_code: "USD".to_string(),
982 min_amount: None,
983 max_amount: None,
984 fiat_amount: 0,
985 payment_method: "lightning".to_string(),
986 premium: 0,
987 buyer_trade_pubkey: None,
988 seller_trade_pubkey: None,
989 buyer_invoice: None,
990 created_at: None,
991 expires_at: None,
992 };
993 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
994 order,
995 slashed_at: 1_734_000_000,
996 });
997 let kind = MessageKind::new(
998 Some(order_id),
999 None,
1000 None,
1001 Action::AddBondInvoice,
1002 Some(payload),
1003 );
1004 assert!(
1005 kind.verify(),
1006 "BondPayoutRequest must verify on AddBondInvoice"
1007 );
1008
1009 let m = Message::Order(kind);
1012 let json = m.as_json().unwrap();
1013 assert!(json.contains("bond_payout_request"));
1014 let back = Message::from_json(&json).unwrap();
1015 assert!(back.verify());
1016 }
1017
1018 #[test]
1019 fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1020 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1023 let order = SmallOrder {
1024 id: Some(order_id),
1025 kind: None,
1026 status: None,
1027 amount: 500,
1028 fiat_code: "USD".to_string(),
1029 min_amount: None,
1030 max_amount: None,
1031 fiat_amount: 0,
1032 payment_method: "lightning".to_string(),
1033 premium: 0,
1034 buyer_trade_pubkey: None,
1035 seller_trade_pubkey: None,
1036 buyer_invoice: None,
1037 created_at: None,
1038 expires_at: None,
1039 };
1040
1041 let _exhaustive: fn(Action) = |a| match a {
1046 Action::AddBondInvoice => {}
1047 Action::NewOrder
1048 | Action::TakeSell
1049 | Action::TakeBuy
1050 | Action::PayInvoice
1051 | Action::PayBondInvoice
1052 | Action::FiatSent
1053 | Action::FiatSentOk
1054 | Action::Release
1055 | Action::Released
1056 | Action::Cancel
1057 | Action::Canceled
1058 | Action::CooperativeCancelInitiatedByYou
1059 | Action::CooperativeCancelInitiatedByPeer
1060 | Action::DisputeInitiatedByYou
1061 | Action::DisputeInitiatedByPeer
1062 | Action::CooperativeCancelAccepted
1063 | Action::BuyerInvoiceAccepted
1064 | Action::BondInvoiceAccepted
1065 | Action::PurchaseCompleted
1066 | Action::BondPayoutCompleted
1067 | Action::BondSlashed
1068 | Action::HoldInvoicePaymentAccepted
1069 | Action::HoldInvoicePaymentSettled
1070 | Action::HoldInvoicePaymentCanceled
1071 | Action::WaitingSellerToPay
1072 | Action::WaitingBuyerInvoice
1073 | Action::AddInvoice
1074 | Action::BuyerTookOrder
1075 | Action::Rate
1076 | Action::RateUser
1077 | Action::RateReceived
1078 | Action::CantDo
1079 | Action::Dispute
1080 | Action::AdminCancel
1081 | Action::AdminCanceled
1082 | Action::AdminSettle
1083 | Action::AdminSettled
1084 | Action::AdminAddSolver
1085 | Action::AdminTakeDispute
1086 | Action::AdminTookDispute
1087 | Action::PaymentFailed
1088 | Action::InvoiceUpdated
1089 | Action::SendDm
1090 | Action::TradePubkey
1091 | Action::RestoreSession
1092 | Action::LastTradeIndex
1093 | Action::Orders => {}
1094 };
1095
1096 let other_actions: &[Action] = &[
1097 Action::NewOrder,
1098 Action::TakeSell,
1099 Action::TakeBuy,
1100 Action::PayInvoice,
1101 Action::PayBondInvoice,
1102 Action::FiatSent,
1103 Action::FiatSentOk,
1104 Action::Release,
1105 Action::Released,
1106 Action::Cancel,
1107 Action::Canceled,
1108 Action::CooperativeCancelInitiatedByYou,
1109 Action::CooperativeCancelInitiatedByPeer,
1110 Action::DisputeInitiatedByYou,
1111 Action::DisputeInitiatedByPeer,
1112 Action::CooperativeCancelAccepted,
1113 Action::BuyerInvoiceAccepted,
1114 Action::BondInvoiceAccepted,
1115 Action::PurchaseCompleted,
1116 Action::BondPayoutCompleted,
1117 Action::BondSlashed,
1118 Action::HoldInvoicePaymentAccepted,
1119 Action::HoldInvoicePaymentSettled,
1120 Action::HoldInvoicePaymentCanceled,
1121 Action::WaitingSellerToPay,
1122 Action::WaitingBuyerInvoice,
1123 Action::AddInvoice,
1124 Action::BuyerTookOrder,
1125 Action::Rate,
1126 Action::RateUser,
1127 Action::RateReceived,
1128 Action::CantDo,
1129 Action::Dispute,
1130 Action::AdminCancel,
1131 Action::AdminCanceled,
1132 Action::AdminSettle,
1133 Action::AdminSettled,
1134 Action::AdminAddSolver,
1135 Action::AdminTakeDispute,
1136 Action::AdminTookDispute,
1137 Action::PaymentFailed,
1138 Action::InvoiceUpdated,
1139 Action::SendDm,
1140 Action::TradePubkey,
1141 Action::RestoreSession,
1142 Action::LastTradeIndex,
1143 Action::Orders,
1144 ];
1145
1146 for action in other_actions {
1147 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1148 order: order.clone(),
1149 slashed_at: 0,
1150 });
1151 let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1152 assert!(
1153 !kind.verify(),
1154 "BondPayoutRequest must be rejected on {action:?}"
1155 );
1156 }
1157 }
1158
1159 #[test]
1160 fn test_bond_payout_ack_actions_verify_and_wire_format() {
1161 use crate::message::BondResolution;
1162
1163 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1164
1165 let order = || SmallOrder {
1168 id: Some(order_id),
1169 kind: None,
1170 status: None,
1171 amount: 500,
1172 fiat_code: "USD".to_string(),
1173 min_amount: None,
1174 max_amount: None,
1175 fiat_amount: 0,
1176 payment_method: "lightning".to_string(),
1177 premium: 0,
1178 buyer_trade_pubkey: None,
1179 seller_trade_pubkey: None,
1180 buyer_invoice: None,
1181 created_at: None,
1182 expires_at: None,
1183 };
1184
1185 for (action, discriminator) in [
1191 (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1192 (Action::BondPayoutCompleted, "bond-payout-completed"),
1193 (Action::BondSlashed, "bond-slashed"),
1194 ] {
1195 let ok = Message::Order(MessageKind::new(
1197 Some(order_id),
1198 Some(1),
1199 Some(2),
1200 action.clone(),
1201 Some(Payload::Order(order())),
1202 ));
1203 assert!(ok.verify(), "{action:?} + Order should verify");
1204
1205 let no_id = Message::Order(MessageKind::new(
1207 None,
1208 Some(1),
1209 Some(2),
1210 action.clone(),
1211 Some(Payload::Order(order())),
1212 ));
1213 assert!(!no_id.verify(), "{action:?} without id must be rejected");
1214
1215 let with_resolution = Message::Order(MessageKind::new(
1217 Some(order_id),
1218 Some(1),
1219 Some(2),
1220 action.clone(),
1221 Some(Payload::BondResolution(BondResolution {
1222 slash_seller: true,
1223 slash_buyer: false,
1224 })),
1225 ));
1226 assert!(
1227 !with_resolution.verify(),
1228 "{action:?} + BondResolution must be rejected"
1229 );
1230
1231 let with_request = Message::Order(MessageKind::new(
1233 Some(order_id),
1234 Some(1),
1235 Some(2),
1236 action.clone(),
1237 Some(Payload::BondPayoutRequest(BondPayoutRequest {
1238 order: order(),
1239 slashed_at: 0,
1240 })),
1241 ));
1242 assert!(
1243 !with_request.verify(),
1244 "{action:?} + BondPayoutRequest must be rejected"
1245 );
1246
1247 let json = ok.as_json().unwrap();
1249 assert!(
1250 json.contains(&format!("\"action\":\"{discriminator}\"")),
1251 "expected kebab-case discriminator {discriminator}, got: {json}"
1252 );
1253 let decoded = Message::from_json(&json).unwrap();
1254 assert!(decoded.verify());
1255 assert_eq!(decoded.inner_action(), Some(action));
1256 }
1257 }
1258
1259 #[test]
1260 fn test_payment_failed_payload() {
1261 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1262
1263 let payment_failed_info = crate::message::PaymentFailedInfo {
1265 payment_attempts: 3,
1266 payment_retries_interval: 60,
1267 };
1268
1269 let payload = Payload::PaymentFailed(payment_failed_info);
1270 let message = Message::Order(MessageKind::new(
1271 Some(uuid),
1272 Some(1),
1273 Some(2),
1274 Action::PaymentFailed,
1275 Some(payload),
1276 ));
1277
1278 assert!(message.verify());
1280
1281 let message_json = message.as_json().unwrap();
1283
1284 let deserialized_message = Message::from_json(&message_json).unwrap();
1286 assert!(deserialized_message.verify());
1287
1288 if let Message::Order(kind) = deserialized_message {
1290 if let Some(Payload::PaymentFailed(info)) = kind.payload {
1291 assert_eq!(info.payment_attempts, 3);
1292 assert_eq!(info.payment_retries_interval, 60);
1293 } else {
1294 panic!("Expected PaymentFailed payload");
1295 }
1296 } else {
1297 panic!("Expected Order message");
1298 }
1299 }
1300
1301 #[test]
1302 fn test_message_payload_signature() {
1303 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1304 let peer = Peer::new(
1305 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1306 None, );
1308 let payload = Payload::Peer(peer);
1309 let test_message = Message::Order(MessageKind::new(
1310 Some(uuid),
1311 Some(1),
1312 Some(2),
1313 Action::FiatSentOk,
1314 Some(payload),
1315 ));
1316 assert!(test_message.verify());
1317 let test_message_json = test_message.as_json().unwrap();
1318 let trade_keys =
1320 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1321 .unwrap();
1322 let sig = Message::sign(test_message_json.clone(), &trade_keys);
1323
1324 assert!(Message::verify_signature(
1325 test_message_json,
1326 trade_keys.public_key(),
1327 sig
1328 ));
1329 }
1330
1331 #[test]
1332 fn test_restore_session_message() {
1333 let restore_request_message = Message::Restore(MessageKind::new(
1335 None,
1336 None,
1337 None,
1338 Action::RestoreSession,
1339 None,
1340 ));
1341
1342 assert!(restore_request_message.verify());
1344 assert_eq!(
1345 restore_request_message.inner_action(),
1346 Some(Action::RestoreSession)
1347 );
1348
1349 let message_json = restore_request_message.as_json().unwrap();
1351 let deserialized_message = Message::from_json(&message_json).unwrap();
1352 assert!(deserialized_message.verify());
1353 assert_eq!(
1354 deserialized_message.inner_action(),
1355 Some(Action::RestoreSession)
1356 );
1357
1358 let restored_orders = vec![
1360 crate::message::RestoredOrdersInfo {
1361 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1362 trade_index: 1,
1363 status: "active".to_string(),
1364 },
1365 crate::message::RestoredOrdersInfo {
1366 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1367 trade_index: 2,
1368 status: "success".to_string(),
1369 },
1370 ];
1371
1372 let restored_disputes = vec![
1373 crate::message::RestoredDisputesInfo {
1374 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1375 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1376 trade_index: 1,
1377 status: "initiated".to_string(),
1378 initiator: Some(crate::message::DisputeInitiator::Buyer),
1379 solver_pubkey: None,
1380 },
1381 crate::message::RestoredDisputesInfo {
1382 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1383 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1384 trade_index: 2,
1385 status: "in-progress".to_string(),
1386 initiator: None,
1387 solver_pubkey: Some(
1388 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1389 ),
1390 },
1391 crate::message::RestoredDisputesInfo {
1392 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1393 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1394 trade_index: 3,
1395 status: "initiated".to_string(),
1396 initiator: Some(crate::message::DisputeInitiator::Seller),
1397 solver_pubkey: None,
1398 },
1399 ];
1400
1401 let restore_session_info = crate::message::RestoreSessionInfo {
1402 restore_orders: restored_orders.clone(),
1403 restore_disputes: restored_disputes.clone(),
1404 };
1405
1406 let restore_data_payload = Payload::RestoreData(restore_session_info);
1407 let restore_data_message = Message::Restore(MessageKind::new(
1408 None,
1409 None,
1410 None,
1411 Action::RestoreSession,
1412 Some(restore_data_payload),
1413 ));
1414
1415 assert!(!restore_data_message.verify());
1417
1418 let message_json = restore_data_message.as_json().unwrap();
1420 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1421
1422 if let Message::Restore(kind) = deserialized_restore_message {
1423 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1424 assert_eq!(session_info.restore_disputes.len(), 3);
1425 assert_eq!(
1426 session_info.restore_disputes[0].initiator,
1427 Some(crate::message::DisputeInitiator::Buyer)
1428 );
1429 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1430 assert_eq!(session_info.restore_disputes[1].initiator, None);
1431 assert_eq!(
1432 session_info.restore_disputes[1].solver_pubkey,
1433 Some(
1434 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1435 .to_string()
1436 )
1437 );
1438 assert_eq!(
1439 session_info.restore_disputes[2].initiator,
1440 Some(crate::message::DisputeInitiator::Seller)
1441 );
1442 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1443 } else {
1444 panic!("Expected RestoreData payload");
1445 }
1446 } else {
1447 panic!("Expected Restore message");
1448 }
1449 }
1450
1451 #[test]
1452 fn test_restore_session_message_validation() {
1453 let restore_request_message = Message::Restore(MessageKind::new(
1455 None,
1456 None,
1457 None,
1458 Action::RestoreSession,
1459 None, ));
1461
1462 assert!(restore_request_message.verify());
1464
1465 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1467 let wrong_message = Message::Restore(MessageKind::new(
1468 None,
1469 None,
1470 None,
1471 Action::RestoreSession,
1472 Some(wrong_payload),
1473 ));
1474
1475 assert!(!wrong_message.verify());
1477
1478 let with_id = Message::Restore(MessageKind::new(
1480 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1481 None,
1482 None,
1483 Action::RestoreSession,
1484 None,
1485 ));
1486 assert!(with_id.verify());
1487
1488 let with_request_id = Message::Restore(MessageKind::new(
1489 None,
1490 Some(42),
1491 None,
1492 Action::RestoreSession,
1493 None,
1494 ));
1495 assert!(with_request_id.verify());
1496
1497 let with_trade_index = Message::Restore(MessageKind::new(
1498 None,
1499 None,
1500 Some(7),
1501 Action::RestoreSession,
1502 None,
1503 ));
1504 assert!(with_trade_index.verify());
1505 }
1506
1507 #[test]
1508 fn test_restore_session_message_constructor() {
1509 let restore_request_message = Message::new_restore(None);
1511
1512 assert!(matches!(restore_request_message, Message::Restore(_)));
1513 assert!(restore_request_message.verify());
1514 assert_eq!(
1515 restore_request_message.inner_action(),
1516 Some(Action::RestoreSession)
1517 );
1518
1519 let restore_session_info = crate::message::RestoreSessionInfo {
1521 restore_orders: vec![],
1522 restore_disputes: vec![],
1523 };
1524 let restore_data_message =
1525 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1526
1527 assert!(matches!(restore_data_message, Message::Restore(_)));
1528 assert!(!restore_data_message.verify());
1529 }
1530
1531 #[test]
1532 fn test_last_trade_index_valid_message() {
1533 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1534 let msg = Message::Restore(kind);
1535
1536 assert!(msg.verify());
1537
1538 let json = msg.as_json().unwrap();
1540 let decoded = Message::from_json(&json).unwrap();
1541 assert!(decoded.verify());
1542
1543 let inner = decoded.get_inner_message_kind();
1545 assert_eq!(inner.trade_index(), 7);
1546 assert_eq!(inner.has_trade_index(), (true, 7));
1547 }
1548
1549 #[test]
1550 fn test_last_trade_index_without_id_is_valid() {
1551 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1553 let msg = Message::Restore(kind);
1554 assert!(msg.verify());
1555 }
1556
1557 #[test]
1558 fn test_last_trade_index_with_payload_fails_validation() {
1559 let kind = MessageKind::new(
1561 None,
1562 None,
1563 Some(3),
1564 Action::LastTradeIndex,
1565 Some(Payload::TextMessage("ignored".to_string())),
1566 );
1567 let msg = Message::Restore(kind);
1568 assert!(!msg.verify());
1569 }
1570
1571 #[test]
1572 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1573 use crate::message::BondResolution;
1574
1575 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1576
1577 for action in [Action::AdminSettle, Action::AdminCancel] {
1578 let with_resolution = Message::Order(MessageKind::new(
1579 Some(uuid),
1580 Some(1),
1581 Some(2),
1582 action.clone(),
1583 Some(Payload::BondResolution(BondResolution {
1584 slash_seller: true,
1585 slash_buyer: false,
1586 })),
1587 ));
1588 assert!(
1589 with_resolution.verify(),
1590 "{action:?} + BondResolution should verify"
1591 );
1592
1593 let without_payload = Message::Order(MessageKind::new(
1594 Some(uuid),
1595 Some(1),
1596 Some(2),
1597 action.clone(),
1598 None,
1599 ));
1600 assert!(without_payload.verify(), "{action:?} + None should verify");
1601
1602 let wrong = Message::Order(MessageKind::new(
1604 Some(uuid),
1605 Some(1),
1606 Some(2),
1607 action.clone(),
1608 Some(Payload::TextMessage("nope".to_string())),
1609 ));
1610 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1611
1612 let no_id = Message::Order(MessageKind::new(
1614 None,
1615 Some(1),
1616 Some(2),
1617 action,
1618 Some(Payload::BondResolution(BondResolution {
1619 slash_seller: false,
1620 slash_buyer: false,
1621 })),
1622 ));
1623 assert!(!no_id.verify(), "admin action without id must be rejected");
1624 }
1625 }
1626
1627 #[test]
1628 fn test_bond_resolution_rejected_on_non_admin_actions() {
1629 use crate::message::BondResolution;
1630
1631 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1632 let payload = Payload::BondResolution(BondResolution {
1633 slash_seller: true,
1634 slash_buyer: true,
1635 });
1636
1637 for action in [
1641 Action::NewOrder,
1642 Action::TakeSell,
1643 Action::TakeBuy,
1644 Action::PayInvoice,
1645 Action::PayBondInvoice,
1646 Action::FiatSent,
1647 Action::FiatSentOk,
1648 Action::Release,
1649 Action::Released,
1650 Action::Cancel,
1651 Action::Canceled,
1652 Action::CooperativeCancelInitiatedByYou,
1653 Action::CooperativeCancelInitiatedByPeer,
1654 Action::DisputeInitiatedByYou,
1655 Action::DisputeInitiatedByPeer,
1656 Action::CooperativeCancelAccepted,
1657 Action::BuyerInvoiceAccepted,
1658 Action::BondInvoiceAccepted,
1659 Action::PurchaseCompleted,
1660 Action::BondPayoutCompleted,
1661 Action::BondSlashed,
1662 Action::HoldInvoicePaymentAccepted,
1663 Action::HoldInvoicePaymentSettled,
1664 Action::HoldInvoicePaymentCanceled,
1665 Action::WaitingSellerToPay,
1666 Action::WaitingBuyerInvoice,
1667 Action::AddInvoice,
1668 Action::AddBondInvoice,
1669 Action::BuyerTookOrder,
1670 Action::Rate,
1671 Action::RateUser,
1672 Action::RateReceived,
1673 Action::CantDo,
1674 Action::Dispute,
1675 Action::AdminCanceled,
1676 Action::AdminSettled,
1677 Action::AdminAddSolver,
1678 Action::AdminTakeDispute,
1679 Action::AdminTookDispute,
1680 Action::PaymentFailed,
1681 Action::InvoiceUpdated,
1682 Action::SendDm,
1683 Action::TradePubkey,
1684 Action::RestoreSession,
1685 Action::LastTradeIndex,
1686 Action::Orders,
1687 ] {
1688 let msg = Message::Order(MessageKind::new(
1689 Some(uuid),
1690 Some(1),
1691 Some(2),
1692 action.clone(),
1693 Some(payload.clone()),
1694 ));
1695 assert!(
1696 !msg.verify(),
1697 "{action:?} must reject BondResolution payload"
1698 );
1699 }
1700 }
1701
1702 #[test]
1703 fn test_bond_resolution_wire_format() {
1704 use crate::message::BondResolution;
1705
1706 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1707 let msg = Message::Order(MessageKind::new(
1708 Some(uuid),
1709 None,
1710 None,
1711 Action::AdminCancel,
1712 Some(Payload::BondResolution(BondResolution {
1713 slash_seller: true,
1714 slash_buyer: false,
1715 })),
1716 ));
1717
1718 let json = msg.as_json().unwrap();
1719 assert!(
1721 json.contains("\"bond_resolution\""),
1722 "expected snake_case discriminator, got: {json}"
1723 );
1724 assert!(json.contains("\"slash_seller\":true"));
1725 assert!(json.contains("\"slash_buyer\":false"));
1726
1727 let decoded = Message::from_json(&json).unwrap();
1729 assert!(decoded.verify());
1730 if let Message::Order(kind) = decoded {
1731 match kind.payload {
1732 Some(Payload::BondResolution(b)) => {
1733 assert!(b.slash_seller);
1734 assert!(!b.slash_buyer);
1735 }
1736 other => panic!("expected BondResolution payload, got {other:?}"),
1737 }
1738 } else {
1739 panic!("expected Order message");
1740 }
1741 }
1742
1743 #[test]
1744 fn test_bond_resolution_legacy_null_payload() {
1745 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1749 let json = format!(
1750 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
1751 );
1752 let msg = Message::from_json(&json).unwrap();
1753 assert!(msg.verify());
1754 }
1755
1756 #[test]
1757 fn test_pay_bond_invoice_wire_format_and_verify() {
1758 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1759 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1760
1761 let msg = Message::Order(MessageKind::new(
1762 Some(uuid),
1763 Some(1),
1764 Some(2),
1765 Action::PayBondInvoice,
1766 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1767 ));
1768 assert!(msg.verify());
1769
1770 let json = msg.as_json().unwrap();
1772 assert!(
1773 json.contains("\"action\":\"pay-bond-invoice\""),
1774 "expected kebab-case discriminator, got: {json}"
1775 );
1776
1777 let decoded = Message::from_json(&json).unwrap();
1779 assert!(decoded.verify());
1780 assert!(matches!(
1781 decoded.inner_action(),
1782 Some(Action::PayBondInvoice)
1783 ));
1784
1785 let no_id = Message::Order(MessageKind::new(
1787 None,
1788 Some(1),
1789 Some(2),
1790 Action::PayBondInvoice,
1791 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1792 ));
1793 assert!(!no_id.verify());
1794
1795 let wrong_payload = Message::Order(MessageKind::new(
1797 Some(uuid),
1798 Some(1),
1799 Some(2),
1800 Action::PayBondInvoice,
1801 Some(Payload::TextMessage("nope".to_string())),
1802 ));
1803 assert!(!wrong_payload.verify());
1804
1805 let no_payload = Message::Order(MessageKind::new(
1807 Some(uuid),
1808 Some(1),
1809 Some(2),
1810 Action::PayBondInvoice,
1811 None,
1812 ));
1813 assert!(!no_payload.verify());
1814 }
1815
1816 #[test]
1817 fn test_add_bond_invoice_wire_format_and_verify() {
1818 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1819 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1820
1821 let msg = Message::Order(MessageKind::new(
1822 Some(uuid),
1823 Some(1),
1824 Some(2),
1825 Action::AddBondInvoice,
1826 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1827 ));
1828 assert!(msg.verify());
1829
1830 let json = msg.as_json().unwrap();
1832 assert!(
1833 json.contains("\"action\":\"add-bond-invoice\""),
1834 "expected kebab-case discriminator, got: {json}"
1835 );
1836
1837 let decoded = Message::from_json(&json).unwrap();
1839 assert!(decoded.verify());
1840 assert!(matches!(
1841 decoded.inner_action(),
1842 Some(Action::AddBondInvoice)
1843 ));
1844
1845 let no_id = Message::Order(MessageKind::new(
1847 None,
1848 Some(1),
1849 Some(2),
1850 Action::AddBondInvoice,
1851 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1852 ));
1853 assert!(!no_id.verify());
1854
1855 let wrong_payload = Message::Order(MessageKind::new(
1857 Some(uuid),
1858 Some(1),
1859 Some(2),
1860 Action::AddBondInvoice,
1861 Some(Payload::TextMessage("nope".to_string())),
1862 ));
1863 assert!(!wrong_payload.verify());
1864
1865 let no_payload = Message::Order(MessageKind::new(
1867 Some(uuid),
1868 Some(1),
1869 Some(2),
1870 Action::AddBondInvoice,
1871 None,
1872 ));
1873 assert!(!no_payload.verify());
1874
1875 if let Message::Order(kind) = &msg {
1877 assert_eq!(kind.get_payment_request(), Some(bolt11));
1878 } else {
1879 panic!("expected Message::Order");
1880 }
1881 }
1882
1883 #[test]
1884 fn test_restored_dispute_helper_serialization_roundtrip() {
1885 use crate::message::RestoredDisputeHelper;
1886
1887 let helper = RestoredDisputeHelper {
1888 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1889 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1890 dispute_status: "initiated".to_string(),
1891 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
1892 master_seller_pubkey: Some("npub1sellerkey".to_string()),
1893 trade_index_buyer: Some(1),
1894 trade_index_seller: Some(2),
1895 buyer_dispute: true,
1896 seller_dispute: false,
1897 solver_pubkey: None,
1898 };
1899
1900 let json = serde_json::to_string(&helper).unwrap();
1901 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
1902
1903 assert_eq!(deserialized.dispute_id, helper.dispute_id);
1904 assert_eq!(deserialized.order_id, helper.order_id);
1905 assert_eq!(deserialized.dispute_status, helper.dispute_status);
1906 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
1907 assert_eq!(
1908 deserialized.master_seller_pubkey,
1909 helper.master_seller_pubkey
1910 );
1911 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
1912 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
1913 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
1914 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
1915 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
1916
1917 let helper_seller_dispute = RestoredDisputeHelper {
1918 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1919 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1920 dispute_status: "in-progress".to_string(),
1921 master_buyer_pubkey: None,
1922 master_seller_pubkey: None,
1923 trade_index_buyer: None,
1924 trade_index_seller: None,
1925 buyer_dispute: false,
1926 seller_dispute: true,
1927 solver_pubkey: Some(
1928 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1929 ),
1930 };
1931
1932 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
1933 let deserialized_seller: RestoredDisputeHelper =
1934 serde_json::from_str(&json_seller).unwrap();
1935
1936 assert_eq!(
1937 deserialized_seller.dispute_id,
1938 helper_seller_dispute.dispute_id
1939 );
1940 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
1941 assert_eq!(
1942 deserialized_seller.dispute_status,
1943 helper_seller_dispute.dispute_status
1944 );
1945 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
1946 assert_eq!(deserialized_seller.master_seller_pubkey, None);
1947 assert_eq!(deserialized_seller.trade_index_buyer, None);
1948 assert_eq!(deserialized_seller.trade_index_seller, None);
1949 assert!(!deserialized_seller.buyer_dispute);
1950 assert!(deserialized_seller.seller_dispute);
1951 assert_eq!(
1952 deserialized_seller.solver_pubkey,
1953 helper_seller_dispute.solver_pubkey
1954 );
1955 }
1956}