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
22use std::fmt;
23use uuid::Uuid;
24
25#[derive(Debug, Deserialize, Serialize, Clone)]
32pub struct Peer {
33 pub pubkey: String,
35 pub reputation: Option<UserInfo>,
38}
39
40impl Peer {
41 pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
43 Self { pubkey, reputation }
44 }
45
46 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
48 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
49 }
50
51 pub fn as_json(&self) -> Result<String, ServiceError> {
53 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
54 }
55}
56
57#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
63#[serde(rename_all = "kebab-case")]
64pub enum Action {
65 NewOrder,
67 TakeSell,
70 TakeBuy,
72 PayInvoice,
75 PayBondInvoice,
82 FiatSent,
84 FiatSentOk,
86 Release,
88 Released,
90 Cancel,
92 Canceled,
94 CooperativeCancelInitiatedByYou,
96 CooperativeCancelInitiatedByPeer,
98 DisputeInitiatedByYou,
100 DisputeInitiatedByPeer,
102 CooperativeCancelAccepted,
104 BuyerInvoiceAccepted,
106 BondInvoiceAccepted,
112 PurchaseCompleted,
114 BondPayoutCompleted,
120 BondSlashed,
128 HoldInvoicePaymentAccepted,
130 HoldInvoicePaymentSettled,
132 HoldInvoicePaymentCanceled,
134 WaitingSellerToPay,
136 WaitingBuyerInvoice,
138 AddInvoice,
141 AddBondInvoice,
148 BuyerTookOrder,
150 Rate,
152 RateUser,
154 RateReceived,
156 CantDo,
158 Dispute,
160 AdminCancel,
162 AdminCanceled,
164 AdminSettle,
166 AdminSettled,
168 AdminAddSolver,
170 AdminTakeDispute,
172 AdminTookDispute,
174 PaymentFailed,
177 InvoiceUpdated,
179 SendDm,
181 TradePubkey,
183 RestoreSession,
185 LastTradeIndex,
188 Orders,
191 AddCashuEscrow,
196 CashuEscrowLocked,
201 CashuPmSignature,
207}
208
209impl fmt::Display for Action {
210 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
211 write!(f, "{self:?}")
212 }
213}
214
215#[derive(Debug, Clone, Deserialize, Serialize)]
222#[serde(rename_all = "kebab-case")]
223pub enum Message {
224 Order(MessageKind),
226 Dispute(MessageKind),
228 CantDo(MessageKind),
230 Rate(MessageKind),
232 Dm(MessageKind),
234 Restore(MessageKind),
236}
237
238impl Message {
239 pub fn new_order(
242 id: Option<Uuid>,
243 request_id: Option<u64>,
244 trade_index: Option<i64>,
245 action: Action,
246 payload: Option<Payload>,
247 ) -> Self {
248 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
249 Self::Order(kind)
250 }
251
252 pub fn new_dispute(
255 id: Option<Uuid>,
256 request_id: Option<u64>,
257 trade_index: Option<i64>,
258 action: Action,
259 payload: Option<Payload>,
260 ) -> Self {
261 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
262
263 Self::Dispute(kind)
264 }
265
266 pub fn new_restore(payload: Option<Payload>) -> Self {
271 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
272 Self::Restore(kind)
273 }
274
275 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
278 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
279
280 Self::CantDo(kind)
281 }
282
283 pub fn new_dm(
285 id: Option<Uuid>,
286 request_id: Option<u64>,
287 action: Action,
288 payload: Option<Payload>,
289 ) -> Self {
290 let kind = MessageKind::new(id, request_id, None, action, payload);
291
292 Self::Dm(kind)
293 }
294
295 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
297 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
298 }
299
300 pub fn as_json(&self) -> Result<String, ServiceError> {
302 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
303 }
304
305 pub fn get_inner_message_kind(&self) -> &MessageKind {
307 match self {
308 Message::Dispute(k)
309 | Message::Order(k)
310 | Message::CantDo(k)
311 | Message::Rate(k)
312 | Message::Dm(k)
313 | Message::Restore(k) => k,
314 }
315 }
316
317 pub fn inner_action(&self) -> Option<Action> {
322 match self {
323 Message::Dispute(a)
324 | Message::Order(a)
325 | Message::CantDo(a)
326 | Message::Rate(a)
327 | Message::Dm(a)
328 | Message::Restore(a) => Some(a.get_action()),
329 }
330 }
331
332 pub fn verify(&self) -> bool {
335 match self {
336 Message::Order(m)
337 | Message::Dispute(m)
338 | Message::CantDo(m)
339 | Message::Rate(m)
340 | Message::Dm(m)
341 | Message::Restore(m) => m.verify(),
342 }
343 }
344
345 pub fn sign(message: String, keys: &Keys) -> Signature {
354 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 keys.sign_schnorr(&message)
359 }
360
361 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
367 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
369 let hash = hash.to_byte_array();
370 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
371
372 let secp = Secp256k1::verification_only();
374 if let Ok(xonlykey) = pubkey.xonly() {
376 xonlykey.verify(&secp, &message, &sig).is_ok()
377 } else {
378 false
379 }
380 }
381}
382
383#[derive(Debug, Clone, Deserialize, Serialize)]
390pub struct MessageKind {
391 pub version: u8,
394 pub request_id: Option<u64>,
397 pub trade_index: Option<i64>,
400 #[serde(skip_serializing_if = "Option::is_none")]
403 pub id: Option<Uuid>,
404 pub action: Action,
406 pub payload: Option<Payload>,
409}
410
411type Amount = i64;
413
414#[derive(Debug, Deserialize, Serialize, Clone)]
419pub struct PaymentFailedInfo {
420 pub payment_attempts: u32,
422 pub payment_retries_interval: u32,
424}
425
426#[cfg_attr(feature = "sqlx", derive(FromRow))]
431#[derive(Debug, Deserialize, Serialize, Clone)]
432pub struct RestoredOrderHelper {
433 pub id: Uuid,
435 pub status: String,
437 pub master_buyer_pubkey: Option<String>,
439 pub master_seller_pubkey: Option<String>,
441 pub trade_index_buyer: Option<i64>,
443 pub trade_index_seller: Option<i64>,
445}
446
447#[cfg_attr(feature = "sqlx", derive(FromRow))]
452#[derive(Debug, Deserialize, Serialize, Clone)]
453pub struct RestoredDisputeHelper {
454 pub dispute_id: Uuid,
456 pub order_id: Uuid,
458 pub dispute_status: String,
460 pub master_buyer_pubkey: Option<String>,
462 pub master_seller_pubkey: Option<String>,
464 pub trade_index_buyer: Option<i64>,
466 pub trade_index_seller: Option<i64>,
468 pub buyer_dispute: bool,
472 pub seller_dispute: bool,
476 pub solver_pubkey: Option<String>,
479}
480
481#[cfg_attr(feature = "sqlx", derive(FromRow))]
483#[derive(Debug, Deserialize, Serialize, Clone)]
484pub struct RestoredOrdersInfo {
485 pub order_id: Uuid,
487 pub trade_index: i64,
489 pub status: String,
491}
492
493#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
495#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
496#[serde(rename_all = "lowercase")]
497#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
498pub enum DisputeInitiator {
499 Buyer,
501 Seller,
503}
504
505#[cfg_attr(feature = "sqlx", derive(FromRow))]
507#[derive(Debug, Deserialize, Serialize, Clone)]
508pub struct RestoredDisputesInfo {
509 pub dispute_id: Uuid,
511 pub order_id: Uuid,
513 pub trade_index: i64,
515 pub status: String,
517 pub initiator: Option<DisputeInitiator>,
520 pub solver_pubkey: Option<String>,
523}
524
525#[derive(Debug, Deserialize, Serialize, Clone, Default)]
530pub struct RestoreSessionInfo {
531 #[serde(rename = "orders")]
533 pub restore_orders: Vec<RestoredOrdersInfo>,
534 #[serde(rename = "disputes")]
536 pub restore_disputes: Vec<RestoredDisputesInfo>,
537}
538
539#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
547pub struct BondResolution {
548 pub slash_seller: bool,
550 pub slash_buyer: bool,
552}
553
554#[derive(Debug, Deserialize, Serialize, Clone)]
569pub struct BondPayoutRequest {
570 pub order: SmallOrder,
574 pub slashed_at: i64,
579}
580
581#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
590pub struct CashuLockProof {
591 pub token: String,
593 pub mint_url: String,
596 pub buyer_pubkey: String,
598 pub seller_pubkey: String,
600 pub mostro_pubkey: String,
603}
604
605impl CashuLockProof {
606 pub fn new(
608 token: String,
609 mint_url: String,
610 buyer_pubkey: String,
611 seller_pubkey: String,
612 mostro_pubkey: String,
613 ) -> Self {
614 Self {
615 token,
616 mint_url,
617 buyer_pubkey,
618 seller_pubkey,
619 mostro_pubkey,
620 }
621 }
622
623 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
625 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
626 }
627
628 pub fn as_json(&self) -> Result<String, ServiceError> {
630 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
631 }
632}
633
634#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
643pub struct CashuProofSignature {
644 pub secret: String,
648 pub signature: String,
651}
652
653impl CashuProofSignature {
654 pub fn new(secret: String, signature: String) -> Self {
656 Self { secret, signature }
657 }
658}
659
660#[derive(Debug, Deserialize, Serialize, Clone)]
666#[serde(rename_all = "snake_case")]
667pub enum Payload {
668 Order(SmallOrder),
670 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
678 TextMessage(String),
680 Peer(Peer),
682 RatingUser(u8),
684 Amount(Amount),
686 Dispute(Uuid, Option<SolverDisputeInfo>),
689 CantDo(Option<CantDoReason>),
691 NextTrade(String, u32),
694 PaymentFailed(PaymentFailedInfo),
696 RestoreData(RestoreSessionInfo),
698 Ids(Vec<Uuid>),
700 Orders(Vec<SmallOrder>),
702 BondResolution(BondResolution),
705 BondPayoutRequest(BondPayoutRequest),
711 CashuLockProof(CashuLockProof),
714 CashuSignatures(Vec<CashuProofSignature>),
720}
721
722#[allow(dead_code)]
723impl MessageKind {
724 pub fn new(
727 id: Option<Uuid>,
728 request_id: Option<u64>,
729 trade_index: Option<i64>,
730 action: Action,
731 payload: Option<Payload>,
732 ) -> Self {
733 Self {
734 version: PROTOCOL_VER,
735 request_id,
736 trade_index,
737 id,
738 action,
739 payload,
740 }
741 }
742 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
744 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
745 }
746 pub fn as_json(&self) -> Result<String, ServiceError> {
748 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
749 }
750
751 pub fn get_action(&self) -> Action {
753 self.action.clone()
754 }
755
756 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
763 match &self.payload {
764 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
765 None => Ok(None),
766 _ => Err(ServiceError::InvalidPayload),
767 }
768 }
769
770 pub fn get_rating(&self) -> Result<u8, ServiceError> {
778 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
779 if !(MIN_RATING..=MAX_RATING).contains(&v) {
780 return Err(ServiceError::InvalidRatingValue);
781 }
782 Ok(v)
783 } else {
784 Err(ServiceError::InvalidRating)
785 }
786 }
787
788 pub fn verify(&self) -> bool {
795 match &self.action {
796 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
797 Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
798 if self.id.is_none() {
799 return false;
800 }
801 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
802 }
803 Action::AddBondInvoice => {
804 if self.id.is_none() {
805 return false;
806 }
807 matches!(
813 &self.payload,
814 Some(Payload::BondPayoutRequest(_)) | Some(Payload::PaymentRequest(_, _, _))
815 )
816 }
817 Action::AdminSettle | Action::AdminCancel => {
818 if self.id.is_none() {
819 return false;
820 }
821 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
822 }
823 Action::AddCashuEscrow => {
824 if self.id.is_none() {
825 return false;
826 }
827 matches!(&self.payload, Some(Payload::CashuLockProof(_)))
828 }
829 Action::CashuPmSignature => {
830 if self.id.is_none() {
831 return false;
832 }
833 matches!(&self.payload, Some(Payload::CashuSignatures(sigs)) if !sigs.is_empty())
834 }
835 Action::TakeSell
836 | Action::TakeBuy
837 | Action::FiatSent
838 | Action::FiatSentOk
839 | Action::Release
840 | Action::Released
841 | Action::Dispute
842 | Action::AdminCanceled
843 | Action::AdminSettled
844 | Action::Rate
845 | Action::RateReceived
846 | Action::AdminTakeDispute
847 | Action::AdminTookDispute
848 | Action::DisputeInitiatedByYou
849 | Action::DisputeInitiatedByPeer
850 | Action::WaitingBuyerInvoice
851 | Action::PurchaseCompleted
852 | Action::BondPayoutCompleted
853 | Action::BondSlashed
854 | Action::HoldInvoicePaymentAccepted
855 | Action::HoldInvoicePaymentSettled
856 | Action::HoldInvoicePaymentCanceled
857 | Action::WaitingSellerToPay
858 | Action::BuyerTookOrder
859 | Action::BuyerInvoiceAccepted
860 | Action::BondInvoiceAccepted
861 | Action::CooperativeCancelInitiatedByYou
862 | Action::CooperativeCancelInitiatedByPeer
863 | Action::CooperativeCancelAccepted
864 | Action::Cancel
865 | Action::InvoiceUpdated
866 | Action::AdminAddSolver
867 | Action::SendDm
868 | Action::TradePubkey
869 | Action::CashuEscrowLocked
870 | Action::Canceled => {
871 if self.id.is_none() {
872 return false;
873 }
874 !matches!(
875 &self.payload,
876 Some(Payload::BondResolution(_)) | Some(Payload::BondPayoutRequest(_))
877 )
878 }
879 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
880 Action::PaymentFailed => {
881 if self.id.is_none() {
882 return false;
883 }
884 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
885 }
886 Action::RateUser => {
887 matches!(&self.payload, Some(Payload::RatingUser(_)))
888 }
889 Action::CantDo => {
890 matches!(&self.payload, Some(Payload::CantDo(_)))
891 }
892 Action::Orders => {
893 matches!(
894 &self.payload,
895 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
896 )
897 }
898 }
899 }
900
901 pub fn get_order(&self) -> Option<&SmallOrder> {
906 if self.action != Action::NewOrder {
907 return None;
908 }
909 match &self.payload {
910 Some(Payload::Order(o)) => Some(o),
911 _ => None,
912 }
913 }
914
915 pub fn get_payment_request(&self) -> Option<String> {
922 if self.action != Action::TakeSell
923 && self.action != Action::AddInvoice
924 && self.action != Action::AddBondInvoice
925 && self.action != Action::NewOrder
926 {
927 return None;
928 }
929 match &self.payload {
930 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
931 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
932 _ => None,
933 }
934 }
935
936 pub fn get_amount(&self) -> Option<Amount> {
940 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
941 return None;
942 }
943 match &self.payload {
944 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
945 Some(Payload::Amount(amount)) => Some(*amount),
946 _ => None,
947 }
948 }
949
950 pub fn get_payload(&self) -> Option<&Payload> {
952 self.payload.as_ref()
953 }
954
955 pub fn has_trade_index(&self) -> (bool, i64) {
958 if let Some(index) = self.trade_index {
959 return (true, index);
960 }
961 (false, 0)
962 }
963
964 pub fn trade_index(&self) -> i64 {
966 if let Some(index) = self.trade_index {
967 return index;
968 }
969 0
970 }
971}
972
973#[cfg(test)]
974mod test {
975 use crate::message::{
976 Action, BondPayoutRequest, CashuLockProof, CashuProofSignature, Message, MessageKind,
977 Payload, Peer,
978 };
979 use crate::order::SmallOrder;
980 use crate::user::UserInfo;
981 use nostr_sdk::Keys;
982 use uuid::uuid;
983
984 #[test]
985 fn test_peer_with_reputation() {
986 let reputation = UserInfo {
988 rating: 4.5,
989 reviews: 10,
990 operating_days: 30,
991 };
992 let peer = Peer::new(
993 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
994 Some(reputation.clone()),
995 );
996
997 assert_eq!(
999 peer.pubkey,
1000 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1001 );
1002 assert!(peer.reputation.is_some());
1003 let peer_reputation = peer.reputation.clone().unwrap();
1004 assert_eq!(peer_reputation.rating, 4.5);
1005 assert_eq!(peer_reputation.reviews, 10);
1006 assert_eq!(peer_reputation.operating_days, 30);
1007
1008 let json = peer.as_json().unwrap();
1010 let deserialized_peer = Peer::from_json(&json).unwrap();
1011 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1012 assert!(deserialized_peer.reputation.is_some());
1013 let deserialized_reputation = deserialized_peer.reputation.unwrap();
1014 assert_eq!(deserialized_reputation.rating, 4.5);
1015 assert_eq!(deserialized_reputation.reviews, 10);
1016 assert_eq!(deserialized_reputation.operating_days, 30);
1017 }
1018
1019 #[test]
1020 fn test_peer_without_reputation() {
1021 let peer = Peer::new(
1023 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1024 None,
1025 );
1026
1027 assert_eq!(
1029 peer.pubkey,
1030 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1031 );
1032 assert!(peer.reputation.is_none());
1033
1034 let json = peer.as_json().unwrap();
1036 let deserialized_peer = Peer::from_json(&json).unwrap();
1037 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1038 assert!(deserialized_peer.reputation.is_none());
1039 }
1040
1041 #[test]
1042 fn test_peer_in_message() {
1043 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1044
1045 let reputation = UserInfo {
1047 rating: 4.5,
1048 reviews: 10,
1049 operating_days: 30,
1050 };
1051 let peer_with_reputation = Peer::new(
1052 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1053 Some(reputation),
1054 );
1055 let payload_with_reputation = Payload::Peer(peer_with_reputation);
1056 let message_with_reputation = Message::Order(MessageKind::new(
1057 Some(uuid),
1058 Some(1),
1059 Some(2),
1060 Action::FiatSentOk,
1061 Some(payload_with_reputation),
1062 ));
1063
1064 assert!(message_with_reputation.verify());
1066 let message_json = message_with_reputation.as_json().unwrap();
1067 let deserialized_message = Message::from_json(&message_json).unwrap();
1068 assert!(deserialized_message.verify());
1069
1070 let peer_without_reputation = Peer::new(
1072 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1073 None,
1074 );
1075 let payload_without_reputation = Payload::Peer(peer_without_reputation);
1076 let message_without_reputation = Message::Order(MessageKind::new(
1077 Some(uuid),
1078 Some(1),
1079 Some(2),
1080 Action::FiatSentOk,
1081 Some(payload_without_reputation),
1082 ));
1083
1084 assert!(message_without_reputation.verify());
1086 let message_json = message_without_reputation.as_json().unwrap();
1087 let deserialized_message = Message::from_json(&message_json).unwrap();
1088 assert!(deserialized_message.verify());
1089 }
1090
1091 #[test]
1092 fn test_bond_payout_request_payload_verifies_on_add_bond_invoice() {
1093 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1094 let order = SmallOrder {
1095 id: Some(order_id),
1096 kind: None,
1097 status: None,
1098 amount: 500,
1099 fiat_code: "USD".to_string(),
1100 min_amount: None,
1101 max_amount: None,
1102 fiat_amount: 0,
1103 payment_method: "lightning".to_string(),
1104 premium: 0,
1105 buyer_trade_pubkey: None,
1106 seller_trade_pubkey: None,
1107 buyer_invoice: None,
1108 created_at: None,
1109 expires_at: None,
1110 };
1111 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1112 order,
1113 slashed_at: 1_734_000_000,
1114 });
1115 let kind = MessageKind::new(
1116 Some(order_id),
1117 None,
1118 None,
1119 Action::AddBondInvoice,
1120 Some(payload),
1121 );
1122 assert!(
1123 kind.verify(),
1124 "BondPayoutRequest must verify on AddBondInvoice"
1125 );
1126
1127 let m = Message::Order(kind);
1130 let json = m.as_json().unwrap();
1131 assert!(json.contains("bond_payout_request"));
1132 let back = Message::from_json(&json).unwrap();
1133 assert!(back.verify());
1134 }
1135
1136 #[test]
1137 fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1138 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1141 let order = SmallOrder {
1142 id: Some(order_id),
1143 kind: None,
1144 status: None,
1145 amount: 500,
1146 fiat_code: "USD".to_string(),
1147 min_amount: None,
1148 max_amount: None,
1149 fiat_amount: 0,
1150 payment_method: "lightning".to_string(),
1151 premium: 0,
1152 buyer_trade_pubkey: None,
1153 seller_trade_pubkey: None,
1154 buyer_invoice: None,
1155 created_at: None,
1156 expires_at: None,
1157 };
1158
1159 let _exhaustive: fn(Action) = |a| match a {
1164 Action::AddBondInvoice => {}
1165 Action::NewOrder
1166 | Action::TakeSell
1167 | Action::TakeBuy
1168 | Action::PayInvoice
1169 | Action::PayBondInvoice
1170 | Action::FiatSent
1171 | Action::FiatSentOk
1172 | Action::Release
1173 | Action::Released
1174 | Action::Cancel
1175 | Action::Canceled
1176 | Action::CooperativeCancelInitiatedByYou
1177 | Action::CooperativeCancelInitiatedByPeer
1178 | Action::DisputeInitiatedByYou
1179 | Action::DisputeInitiatedByPeer
1180 | Action::CooperativeCancelAccepted
1181 | Action::BuyerInvoiceAccepted
1182 | Action::BondInvoiceAccepted
1183 | Action::PurchaseCompleted
1184 | Action::BondPayoutCompleted
1185 | Action::BondSlashed
1186 | Action::HoldInvoicePaymentAccepted
1187 | Action::HoldInvoicePaymentSettled
1188 | Action::HoldInvoicePaymentCanceled
1189 | Action::WaitingSellerToPay
1190 | Action::WaitingBuyerInvoice
1191 | Action::AddInvoice
1192 | Action::BuyerTookOrder
1193 | Action::Rate
1194 | Action::RateUser
1195 | Action::RateReceived
1196 | Action::CantDo
1197 | Action::Dispute
1198 | Action::AdminCancel
1199 | Action::AdminCanceled
1200 | Action::AdminSettle
1201 | Action::AdminSettled
1202 | Action::AdminAddSolver
1203 | Action::AdminTakeDispute
1204 | Action::AdminTookDispute
1205 | Action::PaymentFailed
1206 | Action::InvoiceUpdated
1207 | Action::SendDm
1208 | Action::TradePubkey
1209 | Action::RestoreSession
1210 | Action::LastTradeIndex
1211 | Action::AddCashuEscrow
1212 | Action::CashuEscrowLocked
1213 | Action::CashuPmSignature
1214 | Action::Orders => {}
1215 };
1216
1217 let other_actions: &[Action] = &[
1218 Action::NewOrder,
1219 Action::TakeSell,
1220 Action::TakeBuy,
1221 Action::PayInvoice,
1222 Action::PayBondInvoice,
1223 Action::FiatSent,
1224 Action::FiatSentOk,
1225 Action::Release,
1226 Action::Released,
1227 Action::Cancel,
1228 Action::Canceled,
1229 Action::CooperativeCancelInitiatedByYou,
1230 Action::CooperativeCancelInitiatedByPeer,
1231 Action::DisputeInitiatedByYou,
1232 Action::DisputeInitiatedByPeer,
1233 Action::CooperativeCancelAccepted,
1234 Action::BuyerInvoiceAccepted,
1235 Action::BondInvoiceAccepted,
1236 Action::PurchaseCompleted,
1237 Action::BondPayoutCompleted,
1238 Action::BondSlashed,
1239 Action::HoldInvoicePaymentAccepted,
1240 Action::HoldInvoicePaymentSettled,
1241 Action::HoldInvoicePaymentCanceled,
1242 Action::WaitingSellerToPay,
1243 Action::WaitingBuyerInvoice,
1244 Action::AddInvoice,
1245 Action::BuyerTookOrder,
1246 Action::Rate,
1247 Action::RateUser,
1248 Action::RateReceived,
1249 Action::CantDo,
1250 Action::Dispute,
1251 Action::AdminCancel,
1252 Action::AdminCanceled,
1253 Action::AdminSettle,
1254 Action::AdminSettled,
1255 Action::AdminAddSolver,
1256 Action::AdminTakeDispute,
1257 Action::AdminTookDispute,
1258 Action::PaymentFailed,
1259 Action::InvoiceUpdated,
1260 Action::SendDm,
1261 Action::TradePubkey,
1262 Action::RestoreSession,
1263 Action::LastTradeIndex,
1264 Action::Orders,
1265 Action::AddCashuEscrow,
1266 Action::CashuEscrowLocked,
1267 Action::CashuPmSignature,
1268 ];
1269
1270 for action in other_actions {
1271 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1272 order: order.clone(),
1273 slashed_at: 0,
1274 });
1275 let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1276 assert!(
1277 !kind.verify(),
1278 "BondPayoutRequest must be rejected on {action:?}"
1279 );
1280 }
1281 }
1282
1283 #[test]
1284 fn test_bond_payout_ack_actions_verify_and_wire_format() {
1285 use crate::message::BondResolution;
1286
1287 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1288
1289 let order = || SmallOrder {
1292 id: Some(order_id),
1293 kind: None,
1294 status: None,
1295 amount: 500,
1296 fiat_code: "USD".to_string(),
1297 min_amount: None,
1298 max_amount: None,
1299 fiat_amount: 0,
1300 payment_method: "lightning".to_string(),
1301 premium: 0,
1302 buyer_trade_pubkey: None,
1303 seller_trade_pubkey: None,
1304 buyer_invoice: None,
1305 created_at: None,
1306 expires_at: None,
1307 };
1308
1309 for (action, discriminator) in [
1315 (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1316 (Action::BondPayoutCompleted, "bond-payout-completed"),
1317 (Action::BondSlashed, "bond-slashed"),
1318 ] {
1319 let ok = Message::Order(MessageKind::new(
1321 Some(order_id),
1322 Some(1),
1323 Some(2),
1324 action.clone(),
1325 Some(Payload::Order(order())),
1326 ));
1327 assert!(ok.verify(), "{action:?} + Order should verify");
1328
1329 let no_id = Message::Order(MessageKind::new(
1331 None,
1332 Some(1),
1333 Some(2),
1334 action.clone(),
1335 Some(Payload::Order(order())),
1336 ));
1337 assert!(!no_id.verify(), "{action:?} without id must be rejected");
1338
1339 let with_resolution = Message::Order(MessageKind::new(
1341 Some(order_id),
1342 Some(1),
1343 Some(2),
1344 action.clone(),
1345 Some(Payload::BondResolution(BondResolution {
1346 slash_seller: true,
1347 slash_buyer: false,
1348 })),
1349 ));
1350 assert!(
1351 !with_resolution.verify(),
1352 "{action:?} + BondResolution must be rejected"
1353 );
1354
1355 let with_request = Message::Order(MessageKind::new(
1357 Some(order_id),
1358 Some(1),
1359 Some(2),
1360 action.clone(),
1361 Some(Payload::BondPayoutRequest(BondPayoutRequest {
1362 order: order(),
1363 slashed_at: 0,
1364 })),
1365 ));
1366 assert!(
1367 !with_request.verify(),
1368 "{action:?} + BondPayoutRequest must be rejected"
1369 );
1370
1371 let json = ok.as_json().unwrap();
1373 assert!(
1374 json.contains(&format!("\"action\":\"{discriminator}\"")),
1375 "expected kebab-case discriminator {discriminator}, got: {json}"
1376 );
1377 let decoded = Message::from_json(&json).unwrap();
1378 assert!(decoded.verify());
1379 assert_eq!(decoded.inner_action(), Some(action));
1380 }
1381 }
1382
1383 #[test]
1384 fn test_payment_failed_payload() {
1385 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1386
1387 let payment_failed_info = crate::message::PaymentFailedInfo {
1389 payment_attempts: 3,
1390 payment_retries_interval: 60,
1391 };
1392
1393 let payload = Payload::PaymentFailed(payment_failed_info);
1394 let message = Message::Order(MessageKind::new(
1395 Some(uuid),
1396 Some(1),
1397 Some(2),
1398 Action::PaymentFailed,
1399 Some(payload),
1400 ));
1401
1402 assert!(message.verify());
1404
1405 let message_json = message.as_json().unwrap();
1407
1408 let deserialized_message = Message::from_json(&message_json).unwrap();
1410 assert!(deserialized_message.verify());
1411
1412 if let Message::Order(kind) = deserialized_message {
1414 if let Some(Payload::PaymentFailed(info)) = kind.payload {
1415 assert_eq!(info.payment_attempts, 3);
1416 assert_eq!(info.payment_retries_interval, 60);
1417 } else {
1418 panic!("Expected PaymentFailed payload");
1419 }
1420 } else {
1421 panic!("Expected Order message");
1422 }
1423 }
1424
1425 #[test]
1426 fn test_message_payload_signature() {
1427 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1428 let peer = Peer::new(
1429 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1430 None, );
1432 let payload = Payload::Peer(peer);
1433 let test_message = Message::Order(MessageKind::new(
1434 Some(uuid),
1435 Some(1),
1436 Some(2),
1437 Action::FiatSentOk,
1438 Some(payload),
1439 ));
1440 assert!(test_message.verify());
1441 let test_message_json = test_message.as_json().unwrap();
1442 let trade_keys =
1444 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1445 .unwrap();
1446 let sig = Message::sign(test_message_json.clone(), &trade_keys);
1447
1448 assert!(Message::verify_signature(
1449 test_message_json,
1450 trade_keys.public_key(),
1451 sig
1452 ));
1453 }
1454
1455 #[test]
1456 fn test_restore_session_message() {
1457 let restore_request_message = Message::Restore(MessageKind::new(
1459 None,
1460 None,
1461 None,
1462 Action::RestoreSession,
1463 None,
1464 ));
1465
1466 assert!(restore_request_message.verify());
1468 assert_eq!(
1469 restore_request_message.inner_action(),
1470 Some(Action::RestoreSession)
1471 );
1472
1473 let message_json = restore_request_message.as_json().unwrap();
1475 let deserialized_message = Message::from_json(&message_json).unwrap();
1476 assert!(deserialized_message.verify());
1477 assert_eq!(
1478 deserialized_message.inner_action(),
1479 Some(Action::RestoreSession)
1480 );
1481
1482 let restored_orders = vec![
1484 crate::message::RestoredOrdersInfo {
1485 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1486 trade_index: 1,
1487 status: "active".to_string(),
1488 },
1489 crate::message::RestoredOrdersInfo {
1490 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1491 trade_index: 2,
1492 status: "success".to_string(),
1493 },
1494 ];
1495
1496 let restored_disputes = vec![
1497 crate::message::RestoredDisputesInfo {
1498 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1499 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1500 trade_index: 1,
1501 status: "initiated".to_string(),
1502 initiator: Some(crate::message::DisputeInitiator::Buyer),
1503 solver_pubkey: None,
1504 },
1505 crate::message::RestoredDisputesInfo {
1506 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1507 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1508 trade_index: 2,
1509 status: "in-progress".to_string(),
1510 initiator: None,
1511 solver_pubkey: Some(
1512 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1513 ),
1514 },
1515 crate::message::RestoredDisputesInfo {
1516 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1517 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1518 trade_index: 3,
1519 status: "initiated".to_string(),
1520 initiator: Some(crate::message::DisputeInitiator::Seller),
1521 solver_pubkey: None,
1522 },
1523 ];
1524
1525 let restore_session_info = crate::message::RestoreSessionInfo {
1526 restore_orders: restored_orders.clone(),
1527 restore_disputes: restored_disputes.clone(),
1528 };
1529
1530 let restore_data_payload = Payload::RestoreData(restore_session_info);
1531 let restore_data_message = Message::Restore(MessageKind::new(
1532 None,
1533 None,
1534 None,
1535 Action::RestoreSession,
1536 Some(restore_data_payload),
1537 ));
1538
1539 assert!(!restore_data_message.verify());
1541
1542 let message_json = restore_data_message.as_json().unwrap();
1544 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1545
1546 if let Message::Restore(kind) = deserialized_restore_message {
1547 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1548 assert_eq!(session_info.restore_disputes.len(), 3);
1549 assert_eq!(
1550 session_info.restore_disputes[0].initiator,
1551 Some(crate::message::DisputeInitiator::Buyer)
1552 );
1553 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1554 assert_eq!(session_info.restore_disputes[1].initiator, None);
1555 assert_eq!(
1556 session_info.restore_disputes[1].solver_pubkey,
1557 Some(
1558 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1559 .to_string()
1560 )
1561 );
1562 assert_eq!(
1563 session_info.restore_disputes[2].initiator,
1564 Some(crate::message::DisputeInitiator::Seller)
1565 );
1566 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1567 } else {
1568 panic!("Expected RestoreData payload");
1569 }
1570 } else {
1571 panic!("Expected Restore message");
1572 }
1573 }
1574
1575 #[test]
1576 fn test_restore_session_message_validation() {
1577 let restore_request_message = Message::Restore(MessageKind::new(
1579 None,
1580 None,
1581 None,
1582 Action::RestoreSession,
1583 None, ));
1585
1586 assert!(restore_request_message.verify());
1588
1589 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1591 let wrong_message = Message::Restore(MessageKind::new(
1592 None,
1593 None,
1594 None,
1595 Action::RestoreSession,
1596 Some(wrong_payload),
1597 ));
1598
1599 assert!(!wrong_message.verify());
1601
1602 let with_id = Message::Restore(MessageKind::new(
1604 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1605 None,
1606 None,
1607 Action::RestoreSession,
1608 None,
1609 ));
1610 assert!(with_id.verify());
1611
1612 let with_request_id = Message::Restore(MessageKind::new(
1613 None,
1614 Some(42),
1615 None,
1616 Action::RestoreSession,
1617 None,
1618 ));
1619 assert!(with_request_id.verify());
1620
1621 let with_trade_index = Message::Restore(MessageKind::new(
1622 None,
1623 None,
1624 Some(7),
1625 Action::RestoreSession,
1626 None,
1627 ));
1628 assert!(with_trade_index.verify());
1629 }
1630
1631 #[test]
1632 fn test_restore_session_message_constructor() {
1633 let restore_request_message = Message::new_restore(None);
1635
1636 assert!(matches!(restore_request_message, Message::Restore(_)));
1637 assert!(restore_request_message.verify());
1638 assert_eq!(
1639 restore_request_message.inner_action(),
1640 Some(Action::RestoreSession)
1641 );
1642
1643 let restore_session_info = crate::message::RestoreSessionInfo {
1645 restore_orders: vec![],
1646 restore_disputes: vec![],
1647 };
1648 let restore_data_message =
1649 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1650
1651 assert!(matches!(restore_data_message, Message::Restore(_)));
1652 assert!(!restore_data_message.verify());
1653 }
1654
1655 #[test]
1656 fn test_last_trade_index_valid_message() {
1657 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1658 let msg = Message::Restore(kind);
1659
1660 assert!(msg.verify());
1661
1662 let json = msg.as_json().unwrap();
1664 let decoded = Message::from_json(&json).unwrap();
1665 assert!(decoded.verify());
1666
1667 let inner = decoded.get_inner_message_kind();
1669 assert_eq!(inner.trade_index(), 7);
1670 assert_eq!(inner.has_trade_index(), (true, 7));
1671 }
1672
1673 #[test]
1674 fn test_last_trade_index_without_id_is_valid() {
1675 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1677 let msg = Message::Restore(kind);
1678 assert!(msg.verify());
1679 }
1680
1681 #[test]
1682 fn test_last_trade_index_with_payload_fails_validation() {
1683 let kind = MessageKind::new(
1685 None,
1686 None,
1687 Some(3),
1688 Action::LastTradeIndex,
1689 Some(Payload::TextMessage("ignored".to_string())),
1690 );
1691 let msg = Message::Restore(kind);
1692 assert!(!msg.verify());
1693 }
1694
1695 #[test]
1696 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1697 use crate::message::BondResolution;
1698
1699 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1700
1701 for action in [Action::AdminSettle, Action::AdminCancel] {
1702 let with_resolution = Message::Order(MessageKind::new(
1703 Some(uuid),
1704 Some(1),
1705 Some(2),
1706 action.clone(),
1707 Some(Payload::BondResolution(BondResolution {
1708 slash_seller: true,
1709 slash_buyer: false,
1710 })),
1711 ));
1712 assert!(
1713 with_resolution.verify(),
1714 "{action:?} + BondResolution should verify"
1715 );
1716
1717 let without_payload = Message::Order(MessageKind::new(
1718 Some(uuid),
1719 Some(1),
1720 Some(2),
1721 action.clone(),
1722 None,
1723 ));
1724 assert!(without_payload.verify(), "{action:?} + None should verify");
1725
1726 let wrong = Message::Order(MessageKind::new(
1728 Some(uuid),
1729 Some(1),
1730 Some(2),
1731 action.clone(),
1732 Some(Payload::TextMessage("nope".to_string())),
1733 ));
1734 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1735
1736 let no_id = Message::Order(MessageKind::new(
1738 None,
1739 Some(1),
1740 Some(2),
1741 action,
1742 Some(Payload::BondResolution(BondResolution {
1743 slash_seller: false,
1744 slash_buyer: false,
1745 })),
1746 ));
1747 assert!(!no_id.verify(), "admin action without id must be rejected");
1748 }
1749 }
1750
1751 #[test]
1752 fn test_bond_resolution_rejected_on_non_admin_actions() {
1753 use crate::message::BondResolution;
1754
1755 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1756 let payload = Payload::BondResolution(BondResolution {
1757 slash_seller: true,
1758 slash_buyer: true,
1759 });
1760
1761 for action in [
1765 Action::NewOrder,
1766 Action::TakeSell,
1767 Action::TakeBuy,
1768 Action::PayInvoice,
1769 Action::PayBondInvoice,
1770 Action::FiatSent,
1771 Action::FiatSentOk,
1772 Action::Release,
1773 Action::Released,
1774 Action::Cancel,
1775 Action::Canceled,
1776 Action::CooperativeCancelInitiatedByYou,
1777 Action::CooperativeCancelInitiatedByPeer,
1778 Action::DisputeInitiatedByYou,
1779 Action::DisputeInitiatedByPeer,
1780 Action::CooperativeCancelAccepted,
1781 Action::BuyerInvoiceAccepted,
1782 Action::BondInvoiceAccepted,
1783 Action::PurchaseCompleted,
1784 Action::BondPayoutCompleted,
1785 Action::BondSlashed,
1786 Action::HoldInvoicePaymentAccepted,
1787 Action::HoldInvoicePaymentSettled,
1788 Action::HoldInvoicePaymentCanceled,
1789 Action::WaitingSellerToPay,
1790 Action::WaitingBuyerInvoice,
1791 Action::AddInvoice,
1792 Action::AddBondInvoice,
1793 Action::BuyerTookOrder,
1794 Action::Rate,
1795 Action::RateUser,
1796 Action::RateReceived,
1797 Action::CantDo,
1798 Action::Dispute,
1799 Action::AdminCanceled,
1800 Action::AdminSettled,
1801 Action::AdminAddSolver,
1802 Action::AdminTakeDispute,
1803 Action::AdminTookDispute,
1804 Action::PaymentFailed,
1805 Action::InvoiceUpdated,
1806 Action::SendDm,
1807 Action::TradePubkey,
1808 Action::RestoreSession,
1809 Action::LastTradeIndex,
1810 Action::Orders,
1811 ] {
1812 let msg = Message::Order(MessageKind::new(
1813 Some(uuid),
1814 Some(1),
1815 Some(2),
1816 action.clone(),
1817 Some(payload.clone()),
1818 ));
1819 assert!(
1820 !msg.verify(),
1821 "{action:?} must reject BondResolution payload"
1822 );
1823 }
1824 }
1825
1826 #[test]
1827 fn test_bond_resolution_wire_format() {
1828 use crate::message::BondResolution;
1829
1830 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1831 let msg = Message::Order(MessageKind::new(
1832 Some(uuid),
1833 None,
1834 None,
1835 Action::AdminCancel,
1836 Some(Payload::BondResolution(BondResolution {
1837 slash_seller: true,
1838 slash_buyer: false,
1839 })),
1840 ));
1841
1842 let json = msg.as_json().unwrap();
1843 assert!(
1845 json.contains("\"bond_resolution\""),
1846 "expected snake_case discriminator, got: {json}"
1847 );
1848 assert!(json.contains("\"slash_seller\":true"));
1849 assert!(json.contains("\"slash_buyer\":false"));
1850
1851 let decoded = Message::from_json(&json).unwrap();
1853 assert!(decoded.verify());
1854 if let Message::Order(kind) = decoded {
1855 match kind.payload {
1856 Some(Payload::BondResolution(b)) => {
1857 assert!(b.slash_seller);
1858 assert!(!b.slash_buyer);
1859 }
1860 other => panic!("expected BondResolution payload, got {other:?}"),
1861 }
1862 } else {
1863 panic!("expected Order message");
1864 }
1865 }
1866
1867 #[test]
1868 fn test_bond_resolution_legacy_null_payload() {
1869 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1873 let json = format!(
1874 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
1875 );
1876 let msg = Message::from_json(&json).unwrap();
1877 assert!(msg.verify());
1878 }
1879
1880 #[test]
1881 fn test_pay_bond_invoice_wire_format_and_verify() {
1882 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1883 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1884
1885 let msg = Message::Order(MessageKind::new(
1886 Some(uuid),
1887 Some(1),
1888 Some(2),
1889 Action::PayBondInvoice,
1890 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1891 ));
1892 assert!(msg.verify());
1893
1894 let json = msg.as_json().unwrap();
1896 assert!(
1897 json.contains("\"action\":\"pay-bond-invoice\""),
1898 "expected kebab-case discriminator, got: {json}"
1899 );
1900
1901 let decoded = Message::from_json(&json).unwrap();
1903 assert!(decoded.verify());
1904 assert!(matches!(
1905 decoded.inner_action(),
1906 Some(Action::PayBondInvoice)
1907 ));
1908
1909 let no_id = Message::Order(MessageKind::new(
1911 None,
1912 Some(1),
1913 Some(2),
1914 Action::PayBondInvoice,
1915 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1916 ));
1917 assert!(!no_id.verify());
1918
1919 let wrong_payload = Message::Order(MessageKind::new(
1921 Some(uuid),
1922 Some(1),
1923 Some(2),
1924 Action::PayBondInvoice,
1925 Some(Payload::TextMessage("nope".to_string())),
1926 ));
1927 assert!(!wrong_payload.verify());
1928
1929 let no_payload = Message::Order(MessageKind::new(
1931 Some(uuid),
1932 Some(1),
1933 Some(2),
1934 Action::PayBondInvoice,
1935 None,
1936 ));
1937 assert!(!no_payload.verify());
1938 }
1939
1940 #[test]
1941 fn test_add_bond_invoice_wire_format_and_verify() {
1942 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1943 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1944
1945 let msg = Message::Order(MessageKind::new(
1946 Some(uuid),
1947 Some(1),
1948 Some(2),
1949 Action::AddBondInvoice,
1950 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1951 ));
1952 assert!(msg.verify());
1953
1954 let json = msg.as_json().unwrap();
1956 assert!(
1957 json.contains("\"action\":\"add-bond-invoice\""),
1958 "expected kebab-case discriminator, got: {json}"
1959 );
1960
1961 let decoded = Message::from_json(&json).unwrap();
1963 assert!(decoded.verify());
1964 assert!(matches!(
1965 decoded.inner_action(),
1966 Some(Action::AddBondInvoice)
1967 ));
1968
1969 let no_id = Message::Order(MessageKind::new(
1971 None,
1972 Some(1),
1973 Some(2),
1974 Action::AddBondInvoice,
1975 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1976 ));
1977 assert!(!no_id.verify());
1978
1979 let wrong_payload = Message::Order(MessageKind::new(
1981 Some(uuid),
1982 Some(1),
1983 Some(2),
1984 Action::AddBondInvoice,
1985 Some(Payload::TextMessage("nope".to_string())),
1986 ));
1987 assert!(!wrong_payload.verify());
1988
1989 let no_payload = Message::Order(MessageKind::new(
1991 Some(uuid),
1992 Some(1),
1993 Some(2),
1994 Action::AddBondInvoice,
1995 None,
1996 ));
1997 assert!(!no_payload.verify());
1998
1999 if let Message::Order(kind) = &msg {
2001 assert_eq!(kind.get_payment_request(), Some(bolt11));
2002 } else {
2003 panic!("expected Message::Order");
2004 }
2005 }
2006
2007 #[test]
2008 fn test_restored_dispute_helper_serialization_roundtrip() {
2009 use crate::message::RestoredDisputeHelper;
2010
2011 let helper = RestoredDisputeHelper {
2012 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
2013 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
2014 dispute_status: "initiated".to_string(),
2015 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
2016 master_seller_pubkey: Some("npub1sellerkey".to_string()),
2017 trade_index_buyer: Some(1),
2018 trade_index_seller: Some(2),
2019 buyer_dispute: true,
2020 seller_dispute: false,
2021 solver_pubkey: None,
2022 };
2023
2024 let json = serde_json::to_string(&helper).unwrap();
2025 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
2026
2027 assert_eq!(deserialized.dispute_id, helper.dispute_id);
2028 assert_eq!(deserialized.order_id, helper.order_id);
2029 assert_eq!(deserialized.dispute_status, helper.dispute_status);
2030 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
2031 assert_eq!(
2032 deserialized.master_seller_pubkey,
2033 helper.master_seller_pubkey
2034 );
2035 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
2036 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
2037 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
2038 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
2039 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
2040
2041 let helper_seller_dispute = RestoredDisputeHelper {
2042 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
2043 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
2044 dispute_status: "in-progress".to_string(),
2045 master_buyer_pubkey: None,
2046 master_seller_pubkey: None,
2047 trade_index_buyer: None,
2048 trade_index_seller: None,
2049 buyer_dispute: false,
2050 seller_dispute: true,
2051 solver_pubkey: Some(
2052 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
2053 ),
2054 };
2055
2056 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
2057 let deserialized_seller: RestoredDisputeHelper =
2058 serde_json::from_str(&json_seller).unwrap();
2059
2060 assert_eq!(
2061 deserialized_seller.dispute_id,
2062 helper_seller_dispute.dispute_id
2063 );
2064 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
2065 assert_eq!(
2066 deserialized_seller.dispute_status,
2067 helper_seller_dispute.dispute_status
2068 );
2069 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
2070 assert_eq!(deserialized_seller.master_seller_pubkey, None);
2071 assert_eq!(deserialized_seller.trade_index_buyer, None);
2072 assert_eq!(deserialized_seller.trade_index_seller, None);
2073 assert!(!deserialized_seller.buyer_dispute);
2074 assert!(deserialized_seller.seller_dispute);
2075 assert_eq!(
2076 deserialized_seller.solver_pubkey,
2077 helper_seller_dispute.solver_pubkey
2078 );
2079 }
2080
2081 fn sample_lock_proof() -> CashuLockProof {
2082 CashuLockProof::new(
2083 "cashuAeyJ0b2tlbiI6dGVzdA".to_string(),
2084 "https://mint.example".to_string(),
2085 "02b_buyer".to_string(),
2086 "02s_seller".to_string(),
2087 "02m_mostro".to_string(),
2088 )
2089 }
2090
2091 #[test]
2092 fn test_cashu_lock_proof_json_round_trip() {
2093 let proof = sample_lock_proof();
2094 let json = proof.as_json().unwrap();
2095 let back = CashuLockProof::from_json(&json).unwrap();
2096 assert_eq!(back, proof);
2097 }
2098
2099 #[test]
2100 fn test_add_cashu_escrow_verifies_with_lock_proof() {
2101 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2102 let payload = Payload::CashuLockProof(sample_lock_proof());
2103 let kind = MessageKind::new(
2104 Some(order_id),
2105 None,
2106 None,
2107 Action::AddCashuEscrow,
2108 Some(payload),
2109 );
2110 assert!(
2111 kind.verify(),
2112 "CashuLockProof must verify on AddCashuEscrow"
2113 );
2114
2115 let json = Message::Order(kind).as_json().unwrap();
2118 assert!(json.contains("cashu_lock_proof"));
2119 assert!(Message::from_json(&json).unwrap().verify());
2120 }
2121
2122 #[test]
2123 fn test_add_cashu_escrow_requires_id_and_right_payload() {
2124 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2125
2126 let no_id = MessageKind::new(
2128 None,
2129 None,
2130 None,
2131 Action::AddCashuEscrow,
2132 Some(Payload::CashuLockProof(sample_lock_proof())),
2133 );
2134 assert!(
2135 !no_id.verify(),
2136 "AddCashuEscrow without id must be rejected"
2137 );
2138
2139 let wrong_payload = MessageKind::new(
2141 Some(order_id),
2142 None,
2143 None,
2144 Action::AddCashuEscrow,
2145 Some(Payload::TextMessage("not a lock proof".to_string())),
2146 );
2147 assert!(
2148 !wrong_payload.verify(),
2149 "AddCashuEscrow with non-lock-proof payload must be rejected"
2150 );
2151 }
2152
2153 #[test]
2154 fn test_cashu_pm_signature_verifies_with_signatures() {
2155 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2156 let kind = MessageKind::new(
2157 Some(order_id),
2158 None,
2159 None,
2160 Action::CashuPmSignature,
2161 Some(Payload::CashuSignatures(vec![
2162 CashuProofSignature::new("secret-0".to_string(), "deadbeef".to_string()),
2163 CashuProofSignature::new("secret-1".to_string(), "c0ffee".to_string()),
2164 ])),
2165 );
2166 assert!(
2167 kind.verify(),
2168 "CashuSignatures must verify on CashuPmSignature"
2169 );
2170
2171 let json = Message::Order(kind).as_json().unwrap();
2172 assert!(json.contains("cashu_signatures"));
2173 assert!(Message::from_json(&json).unwrap().verify());
2174
2175 let wrong = MessageKind::new(Some(order_id), None, None, Action::CashuPmSignature, None);
2177 assert!(
2178 !wrong.verify(),
2179 "CashuPmSignature without a signature payload must be rejected"
2180 );
2181
2182 let empty = MessageKind::new(
2185 Some(order_id),
2186 None,
2187 None,
2188 Action::CashuPmSignature,
2189 Some(Payload::CashuSignatures(vec![])),
2190 );
2191 assert!(
2192 !empty.verify(),
2193 "CashuPmSignature with an empty signature set must be rejected"
2194 );
2195 }
2196
2197 #[test]
2198 fn test_cashu_escrow_locked_is_informational() {
2199 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2200
2201 let ok = MessageKind::new(Some(order_id), None, None, Action::CashuEscrowLocked, None);
2203 assert!(ok.verify(), "CashuEscrowLocked with id must verify");
2204
2205 let no_id = MessageKind::new(None, None, None, Action::CashuEscrowLocked, None);
2207 assert!(
2208 !no_id.verify(),
2209 "CashuEscrowLocked without id must be rejected"
2210 );
2211 }
2212}