1use crate::prelude::*;
14use bitcoin::hashes::sha256::Hash as Sha256Hash;
15use bitcoin::hashes::Hash;
16use nostr_sdk::prelude::*;
17use secp256k1::schnorr;
18use secp256k1::Secp256k1;
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 {
357 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
358 keys.sign_schnorr(hash.to_byte_array())
359 }
360
361 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
371 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
372 let hash = hash.to_byte_array();
373
374 let secp = Secp256k1::verification_only();
375 if let Ok(xonlykey) = pubkey.xonly() {
376 let sig = schnorr::Signature::from_byte_array(*sig.as_bytes());
377 xonlykey.verify(&secp, &hash, &sig).is_ok()
378 } else {
379 false
380 }
381 }
382}
383
384#[derive(Debug, Clone, Deserialize, Serialize)]
391pub struct MessageKind {
392 pub version: u8,
395 pub request_id: Option<u64>,
398 pub trade_index: Option<i64>,
401 #[serde(skip_serializing_if = "Option::is_none")]
404 pub id: Option<Uuid>,
405 pub action: Action,
407 pub payload: Option<Payload>,
410}
411
412type Amount = i64;
414
415#[derive(Debug, Deserialize, Serialize, Clone)]
420pub struct PaymentFailedInfo {
421 pub payment_attempts: u32,
423 pub payment_retries_interval: u32,
425}
426
427#[cfg_attr(feature = "sqlx", derive(FromRow))]
432#[derive(Debug, Deserialize, Serialize, Clone)]
433pub struct RestoredOrderHelper {
434 pub id: Uuid,
436 pub status: String,
438 pub master_buyer_pubkey: Option<String>,
440 pub master_seller_pubkey: Option<String>,
442 pub trade_index_buyer: Option<i64>,
444 pub trade_index_seller: Option<i64>,
446}
447
448#[cfg_attr(feature = "sqlx", derive(FromRow))]
453#[derive(Debug, Deserialize, Serialize, Clone)]
454pub struct RestoredDisputeHelper {
455 pub dispute_id: Uuid,
457 pub order_id: Uuid,
459 pub dispute_status: String,
461 pub master_buyer_pubkey: Option<String>,
463 pub master_seller_pubkey: Option<String>,
465 pub trade_index_buyer: Option<i64>,
467 pub trade_index_seller: Option<i64>,
469 pub buyer_dispute: bool,
473 pub seller_dispute: bool,
477 pub solver_pubkey: Option<String>,
480}
481
482#[cfg_attr(feature = "sqlx", derive(FromRow))]
484#[derive(Debug, Deserialize, Serialize, Clone)]
485pub struct RestoredOrdersInfo {
486 pub order_id: Uuid,
488 pub trade_index: i64,
490 pub status: String,
492}
493
494#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
496#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
497#[serde(rename_all = "lowercase")]
498#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
499pub enum DisputeInitiator {
500 Buyer,
502 Seller,
504}
505
506#[cfg_attr(feature = "sqlx", derive(FromRow))]
508#[derive(Debug, Deserialize, Serialize, Clone)]
509pub struct RestoredDisputesInfo {
510 pub dispute_id: Uuid,
512 pub order_id: Uuid,
514 pub trade_index: i64,
516 pub status: String,
518 pub initiator: Option<DisputeInitiator>,
521 pub solver_pubkey: Option<String>,
524}
525
526#[derive(Debug, Deserialize, Serialize, Clone, Default)]
531pub struct RestoreSessionInfo {
532 #[serde(rename = "orders")]
534 pub restore_orders: Vec<RestoredOrdersInfo>,
535 #[serde(rename = "disputes")]
537 pub restore_disputes: Vec<RestoredDisputesInfo>,
538}
539
540#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
548pub struct BondResolution {
549 pub slash_seller: bool,
551 pub slash_buyer: bool,
553}
554
555#[derive(Debug, Deserialize, Serialize, Clone)]
570pub struct BondPayoutRequest {
571 pub order: SmallOrder,
575 pub slashed_at: i64,
580}
581
582#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
591pub struct CashuLockProof {
592 pub token: String,
594 pub mint_url: String,
597 pub buyer_pubkey: String,
599 pub seller_pubkey: String,
601 pub mostro_pubkey: String,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
609 pub fee_token: Option<String>,
610}
611
612impl CashuLockProof {
613 pub fn new(
615 token: String,
616 mint_url: String,
617 buyer_pubkey: String,
618 seller_pubkey: String,
619 mostro_pubkey: String,
620 ) -> Self {
621 Self {
622 token,
623 mint_url,
624 buyer_pubkey,
625 seller_pubkey,
626 mostro_pubkey,
627 fee_token: None,
628 }
629 }
630
631 pub fn with_fee_token(mut self, fee_token: String) -> Self {
634 self.fee_token = Some(fee_token);
635 self
636 }
637
638 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
640 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
641 }
642
643 pub fn as_json(&self) -> Result<String, ServiceError> {
645 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
646 }
647}
648
649#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
658pub struct CashuProofSignature {
659 pub secret: String,
663 pub signature: String,
666}
667
668impl CashuProofSignature {
669 pub fn new(secret: String, signature: String) -> Self {
671 Self { secret, signature }
672 }
673}
674
675#[derive(Debug, Deserialize, Serialize, Clone)]
681#[serde(rename_all = "snake_case")]
682pub enum Payload {
683 Order(SmallOrder),
685 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
693 TextMessage(String),
695 Peer(Peer),
697 RatingUser(u8),
699 Amount(Amount),
701 Dispute(Uuid, Option<SolverDisputeInfo>),
704 CantDo(Option<CantDoReason>),
706 NextTrade(String, u32),
709 PaymentFailed(PaymentFailedInfo),
711 RestoreData(RestoreSessionInfo),
713 Ids(Vec<Uuid>),
715 Orders(Vec<SmallOrder>),
717 BondResolution(BondResolution),
720 BondPayoutRequest(BondPayoutRequest),
726 CashuLockProof(CashuLockProof),
729 CashuSignatures(Vec<CashuProofSignature>),
735}
736
737#[allow(dead_code)]
738impl MessageKind {
739 pub fn new(
742 id: Option<Uuid>,
743 request_id: Option<u64>,
744 trade_index: Option<i64>,
745 action: Action,
746 payload: Option<Payload>,
747 ) -> Self {
748 Self {
749 version: PROTOCOL_VER,
750 request_id,
751 trade_index,
752 id,
753 action,
754 payload,
755 }
756 }
757 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
759 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
760 }
761 pub fn as_json(&self) -> Result<String, ServiceError> {
763 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
764 }
765
766 pub fn get_action(&self) -> Action {
768 self.action.clone()
769 }
770
771 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
778 match &self.payload {
779 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
780 None => Ok(None),
781 _ => Err(ServiceError::InvalidPayload),
782 }
783 }
784
785 pub fn get_rating(&self) -> Result<u8, ServiceError> {
793 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
794 if !(MIN_RATING..=MAX_RATING).contains(&v) {
795 return Err(ServiceError::InvalidRatingValue);
796 }
797 Ok(v)
798 } else {
799 Err(ServiceError::InvalidRating)
800 }
801 }
802
803 pub fn verify(&self) -> bool {
810 match &self.action {
811 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
812 Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
813 if self.id.is_none() {
814 return false;
815 }
816 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
817 }
818 Action::AddBondInvoice => {
819 if self.id.is_none() {
820 return false;
821 }
822 matches!(
828 &self.payload,
829 Some(Payload::BondPayoutRequest(_)) | Some(Payload::PaymentRequest(_, _, _))
830 )
831 }
832 Action::AdminSettle | Action::AdminCancel => {
833 if self.id.is_none() {
834 return false;
835 }
836 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
837 }
838 Action::AddCashuEscrow => {
839 if self.id.is_none() {
840 return false;
841 }
842 matches!(&self.payload, Some(Payload::CashuLockProof(_)))
843 }
844 Action::CashuPmSignature => {
845 if self.id.is_none() {
846 return false;
847 }
848 matches!(&self.payload, Some(Payload::CashuSignatures(sigs)) if !sigs.is_empty())
849 }
850 Action::TakeSell
851 | Action::TakeBuy
852 | Action::FiatSent
853 | Action::FiatSentOk
854 | Action::Release
855 | Action::Released
856 | Action::Dispute
857 | Action::AdminCanceled
858 | Action::AdminSettled
859 | Action::Rate
860 | Action::RateReceived
861 | Action::AdminTakeDispute
862 | Action::AdminTookDispute
863 | Action::DisputeInitiatedByYou
864 | Action::DisputeInitiatedByPeer
865 | Action::WaitingBuyerInvoice
866 | Action::PurchaseCompleted
867 | Action::BondPayoutCompleted
868 | Action::BondSlashed
869 | Action::HoldInvoicePaymentAccepted
870 | Action::HoldInvoicePaymentSettled
871 | Action::HoldInvoicePaymentCanceled
872 | Action::WaitingSellerToPay
873 | Action::BuyerTookOrder
874 | Action::BuyerInvoiceAccepted
875 | Action::BondInvoiceAccepted
876 | Action::CooperativeCancelInitiatedByYou
877 | Action::CooperativeCancelInitiatedByPeer
878 | Action::CooperativeCancelAccepted
879 | Action::Cancel
880 | Action::InvoiceUpdated
881 | Action::AdminAddSolver
882 | Action::SendDm
883 | Action::TradePubkey
884 | Action::CashuEscrowLocked
885 | Action::Canceled => {
886 if self.id.is_none() {
887 return false;
888 }
889 !matches!(
890 &self.payload,
891 Some(Payload::BondResolution(_)) | Some(Payload::BondPayoutRequest(_))
892 )
893 }
894 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
895 Action::PaymentFailed => {
896 if self.id.is_none() {
897 return false;
898 }
899 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
900 }
901 Action::RateUser => {
902 matches!(&self.payload, Some(Payload::RatingUser(_)))
903 }
904 Action::CantDo => {
905 matches!(&self.payload, Some(Payload::CantDo(_)))
906 }
907 Action::Orders => {
908 matches!(
909 &self.payload,
910 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
911 )
912 }
913 }
914 }
915
916 pub fn get_order(&self) -> Option<&SmallOrder> {
921 if self.action != Action::NewOrder {
922 return None;
923 }
924 match &self.payload {
925 Some(Payload::Order(o)) => Some(o),
926 _ => None,
927 }
928 }
929
930 pub fn get_payment_request(&self) -> Option<String> {
937 if self.action != Action::TakeSell
938 && self.action != Action::AddInvoice
939 && self.action != Action::AddBondInvoice
940 && self.action != Action::NewOrder
941 {
942 return None;
943 }
944 match &self.payload {
945 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
946 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
947 _ => None,
948 }
949 }
950
951 pub fn get_amount(&self) -> Option<Amount> {
955 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
956 return None;
957 }
958 match &self.payload {
959 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
960 Some(Payload::Amount(amount)) => Some(*amount),
961 _ => None,
962 }
963 }
964
965 pub fn get_payload(&self) -> Option<&Payload> {
967 self.payload.as_ref()
968 }
969
970 pub fn has_trade_index(&self) -> (bool, i64) {
973 if let Some(index) = self.trade_index {
974 return (true, index);
975 }
976 (false, 0)
977 }
978
979 pub fn trade_index(&self) -> i64 {
981 if let Some(index) = self.trade_index {
982 return index;
983 }
984 0
985 }
986}
987
988#[cfg(test)]
989mod test {
990 use crate::message::{
991 Action, BondPayoutRequest, CashuLockProof, CashuProofSignature, Message, MessageKind,
992 Payload, Peer,
993 };
994 use crate::order::SmallOrder;
995 use crate::user::UserInfo;
996 use nostr_sdk::prelude::Keys;
997 use uuid::uuid;
998
999 #[test]
1000 fn test_peer_with_reputation() {
1001 let reputation = UserInfo {
1003 rating: 4.5,
1004 reviews: 10,
1005 operating_days: 30,
1006 };
1007 let peer = Peer::new(
1008 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1009 Some(reputation.clone()),
1010 );
1011
1012 assert_eq!(
1014 peer.pubkey,
1015 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1016 );
1017 assert!(peer.reputation.is_some());
1018 let peer_reputation = peer.reputation.clone().unwrap();
1019 assert_eq!(peer_reputation.rating, 4.5);
1020 assert_eq!(peer_reputation.reviews, 10);
1021 assert_eq!(peer_reputation.operating_days, 30);
1022
1023 let json = peer.as_json().unwrap();
1025 let deserialized_peer = Peer::from_json(&json).unwrap();
1026 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1027 assert!(deserialized_peer.reputation.is_some());
1028 let deserialized_reputation = deserialized_peer.reputation.unwrap();
1029 assert_eq!(deserialized_reputation.rating, 4.5);
1030 assert_eq!(deserialized_reputation.reviews, 10);
1031 assert_eq!(deserialized_reputation.operating_days, 30);
1032 }
1033
1034 #[test]
1035 fn test_peer_without_reputation() {
1036 let peer = Peer::new(
1038 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1039 None,
1040 );
1041
1042 assert_eq!(
1044 peer.pubkey,
1045 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1046 );
1047 assert!(peer.reputation.is_none());
1048
1049 let json = peer.as_json().unwrap();
1051 let deserialized_peer = Peer::from_json(&json).unwrap();
1052 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1053 assert!(deserialized_peer.reputation.is_none());
1054 }
1055
1056 #[test]
1057 fn test_peer_in_message() {
1058 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1059
1060 let reputation = UserInfo {
1062 rating: 4.5,
1063 reviews: 10,
1064 operating_days: 30,
1065 };
1066 let peer_with_reputation = Peer::new(
1067 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1068 Some(reputation),
1069 );
1070 let payload_with_reputation = Payload::Peer(peer_with_reputation);
1071 let message_with_reputation = Message::Order(MessageKind::new(
1072 Some(uuid),
1073 Some(1),
1074 Some(2),
1075 Action::FiatSentOk,
1076 Some(payload_with_reputation),
1077 ));
1078
1079 assert!(message_with_reputation.verify());
1081 let message_json = message_with_reputation.as_json().unwrap();
1082 let deserialized_message = Message::from_json(&message_json).unwrap();
1083 assert!(deserialized_message.verify());
1084
1085 let peer_without_reputation = Peer::new(
1087 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1088 None,
1089 );
1090 let payload_without_reputation = Payload::Peer(peer_without_reputation);
1091 let message_without_reputation = Message::Order(MessageKind::new(
1092 Some(uuid),
1093 Some(1),
1094 Some(2),
1095 Action::FiatSentOk,
1096 Some(payload_without_reputation),
1097 ));
1098
1099 assert!(message_without_reputation.verify());
1101 let message_json = message_without_reputation.as_json().unwrap();
1102 let deserialized_message = Message::from_json(&message_json).unwrap();
1103 assert!(deserialized_message.verify());
1104 }
1105
1106 #[test]
1107 fn test_bond_payout_request_payload_verifies_on_add_bond_invoice() {
1108 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1109 let order = SmallOrder {
1110 id: Some(order_id),
1111 kind: None,
1112 status: None,
1113 amount: 500,
1114 fiat_code: "USD".to_string(),
1115 min_amount: None,
1116 max_amount: None,
1117 fiat_amount: 0,
1118 payment_method: "lightning".to_string(),
1119 premium: 0,
1120 buyer_trade_pubkey: None,
1121 seller_trade_pubkey: None,
1122 buyer_invoice: None,
1123 created_at: None,
1124 expires_at: None,
1125 };
1126 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1127 order,
1128 slashed_at: 1_734_000_000,
1129 });
1130 let kind = MessageKind::new(
1131 Some(order_id),
1132 None,
1133 None,
1134 Action::AddBondInvoice,
1135 Some(payload),
1136 );
1137 assert!(
1138 kind.verify(),
1139 "BondPayoutRequest must verify on AddBondInvoice"
1140 );
1141
1142 let m = Message::Order(kind);
1145 let json = m.as_json().unwrap();
1146 assert!(json.contains("bond_payout_request"));
1147 let back = Message::from_json(&json).unwrap();
1148 assert!(back.verify());
1149 }
1150
1151 #[test]
1152 fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1153 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1156 let order = SmallOrder {
1157 id: Some(order_id),
1158 kind: None,
1159 status: None,
1160 amount: 500,
1161 fiat_code: "USD".to_string(),
1162 min_amount: None,
1163 max_amount: None,
1164 fiat_amount: 0,
1165 payment_method: "lightning".to_string(),
1166 premium: 0,
1167 buyer_trade_pubkey: None,
1168 seller_trade_pubkey: None,
1169 buyer_invoice: None,
1170 created_at: None,
1171 expires_at: None,
1172 };
1173
1174 let _exhaustive: fn(Action) = |a| match a {
1179 Action::AddBondInvoice => {}
1180 Action::NewOrder
1181 | Action::TakeSell
1182 | Action::TakeBuy
1183 | Action::PayInvoice
1184 | Action::PayBondInvoice
1185 | Action::FiatSent
1186 | Action::FiatSentOk
1187 | Action::Release
1188 | Action::Released
1189 | Action::Cancel
1190 | Action::Canceled
1191 | Action::CooperativeCancelInitiatedByYou
1192 | Action::CooperativeCancelInitiatedByPeer
1193 | Action::DisputeInitiatedByYou
1194 | Action::DisputeInitiatedByPeer
1195 | Action::CooperativeCancelAccepted
1196 | Action::BuyerInvoiceAccepted
1197 | Action::BondInvoiceAccepted
1198 | Action::PurchaseCompleted
1199 | Action::BondPayoutCompleted
1200 | Action::BondSlashed
1201 | Action::HoldInvoicePaymentAccepted
1202 | Action::HoldInvoicePaymentSettled
1203 | Action::HoldInvoicePaymentCanceled
1204 | Action::WaitingSellerToPay
1205 | Action::WaitingBuyerInvoice
1206 | Action::AddInvoice
1207 | Action::BuyerTookOrder
1208 | Action::Rate
1209 | Action::RateUser
1210 | Action::RateReceived
1211 | Action::CantDo
1212 | Action::Dispute
1213 | Action::AdminCancel
1214 | Action::AdminCanceled
1215 | Action::AdminSettle
1216 | Action::AdminSettled
1217 | Action::AdminAddSolver
1218 | Action::AdminTakeDispute
1219 | Action::AdminTookDispute
1220 | Action::PaymentFailed
1221 | Action::InvoiceUpdated
1222 | Action::SendDm
1223 | Action::TradePubkey
1224 | Action::RestoreSession
1225 | Action::LastTradeIndex
1226 | Action::AddCashuEscrow
1227 | Action::CashuEscrowLocked
1228 | Action::CashuPmSignature
1229 | Action::Orders => {}
1230 };
1231
1232 let other_actions: &[Action] = &[
1233 Action::NewOrder,
1234 Action::TakeSell,
1235 Action::TakeBuy,
1236 Action::PayInvoice,
1237 Action::PayBondInvoice,
1238 Action::FiatSent,
1239 Action::FiatSentOk,
1240 Action::Release,
1241 Action::Released,
1242 Action::Cancel,
1243 Action::Canceled,
1244 Action::CooperativeCancelInitiatedByYou,
1245 Action::CooperativeCancelInitiatedByPeer,
1246 Action::DisputeInitiatedByYou,
1247 Action::DisputeInitiatedByPeer,
1248 Action::CooperativeCancelAccepted,
1249 Action::BuyerInvoiceAccepted,
1250 Action::BondInvoiceAccepted,
1251 Action::PurchaseCompleted,
1252 Action::BondPayoutCompleted,
1253 Action::BondSlashed,
1254 Action::HoldInvoicePaymentAccepted,
1255 Action::HoldInvoicePaymentSettled,
1256 Action::HoldInvoicePaymentCanceled,
1257 Action::WaitingSellerToPay,
1258 Action::WaitingBuyerInvoice,
1259 Action::AddInvoice,
1260 Action::BuyerTookOrder,
1261 Action::Rate,
1262 Action::RateUser,
1263 Action::RateReceived,
1264 Action::CantDo,
1265 Action::Dispute,
1266 Action::AdminCancel,
1267 Action::AdminCanceled,
1268 Action::AdminSettle,
1269 Action::AdminSettled,
1270 Action::AdminAddSolver,
1271 Action::AdminTakeDispute,
1272 Action::AdminTookDispute,
1273 Action::PaymentFailed,
1274 Action::InvoiceUpdated,
1275 Action::SendDm,
1276 Action::TradePubkey,
1277 Action::RestoreSession,
1278 Action::LastTradeIndex,
1279 Action::Orders,
1280 Action::AddCashuEscrow,
1281 Action::CashuEscrowLocked,
1282 Action::CashuPmSignature,
1283 ];
1284
1285 for action in other_actions {
1286 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1287 order: order.clone(),
1288 slashed_at: 0,
1289 });
1290 let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1291 assert!(
1292 !kind.verify(),
1293 "BondPayoutRequest must be rejected on {action:?}"
1294 );
1295 }
1296 }
1297
1298 #[test]
1299 fn test_bond_payout_ack_actions_verify_and_wire_format() {
1300 use crate::message::BondResolution;
1301
1302 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1303
1304 let order = || SmallOrder {
1307 id: Some(order_id),
1308 kind: None,
1309 status: None,
1310 amount: 500,
1311 fiat_code: "USD".to_string(),
1312 min_amount: None,
1313 max_amount: None,
1314 fiat_amount: 0,
1315 payment_method: "lightning".to_string(),
1316 premium: 0,
1317 buyer_trade_pubkey: None,
1318 seller_trade_pubkey: None,
1319 buyer_invoice: None,
1320 created_at: None,
1321 expires_at: None,
1322 };
1323
1324 for (action, discriminator) in [
1330 (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1331 (Action::BondPayoutCompleted, "bond-payout-completed"),
1332 (Action::BondSlashed, "bond-slashed"),
1333 ] {
1334 let ok = Message::Order(MessageKind::new(
1336 Some(order_id),
1337 Some(1),
1338 Some(2),
1339 action.clone(),
1340 Some(Payload::Order(order())),
1341 ));
1342 assert!(ok.verify(), "{action:?} + Order should verify");
1343
1344 let no_id = Message::Order(MessageKind::new(
1346 None,
1347 Some(1),
1348 Some(2),
1349 action.clone(),
1350 Some(Payload::Order(order())),
1351 ));
1352 assert!(!no_id.verify(), "{action:?} without id must be rejected");
1353
1354 let with_resolution = Message::Order(MessageKind::new(
1356 Some(order_id),
1357 Some(1),
1358 Some(2),
1359 action.clone(),
1360 Some(Payload::BondResolution(BondResolution {
1361 slash_seller: true,
1362 slash_buyer: false,
1363 })),
1364 ));
1365 assert!(
1366 !with_resolution.verify(),
1367 "{action:?} + BondResolution must be rejected"
1368 );
1369
1370 let with_request = Message::Order(MessageKind::new(
1372 Some(order_id),
1373 Some(1),
1374 Some(2),
1375 action.clone(),
1376 Some(Payload::BondPayoutRequest(BondPayoutRequest {
1377 order: order(),
1378 slashed_at: 0,
1379 })),
1380 ));
1381 assert!(
1382 !with_request.verify(),
1383 "{action:?} + BondPayoutRequest must be rejected"
1384 );
1385
1386 let json = ok.as_json().unwrap();
1388 assert!(
1389 json.contains(&format!("\"action\":\"{discriminator}\"")),
1390 "expected kebab-case discriminator {discriminator}, got: {json}"
1391 );
1392 let decoded = Message::from_json(&json).unwrap();
1393 assert!(decoded.verify());
1394 assert_eq!(decoded.inner_action(), Some(action));
1395 }
1396 }
1397
1398 #[test]
1399 fn test_payment_failed_payload() {
1400 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1401
1402 let payment_failed_info = crate::message::PaymentFailedInfo {
1404 payment_attempts: 3,
1405 payment_retries_interval: 60,
1406 };
1407
1408 let payload = Payload::PaymentFailed(payment_failed_info);
1409 let message = Message::Order(MessageKind::new(
1410 Some(uuid),
1411 Some(1),
1412 Some(2),
1413 Action::PaymentFailed,
1414 Some(payload),
1415 ));
1416
1417 assert!(message.verify());
1419
1420 let message_json = message.as_json().unwrap();
1422
1423 let deserialized_message = Message::from_json(&message_json).unwrap();
1425 assert!(deserialized_message.verify());
1426
1427 if let Message::Order(kind) = deserialized_message {
1429 if let Some(Payload::PaymentFailed(info)) = kind.payload {
1430 assert_eq!(info.payment_attempts, 3);
1431 assert_eq!(info.payment_retries_interval, 60);
1432 } else {
1433 panic!("Expected PaymentFailed payload");
1434 }
1435 } else {
1436 panic!("Expected Order message");
1437 }
1438 }
1439
1440 #[test]
1441 fn test_message_payload_signature() {
1442 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1443 let peer = Peer::new(
1444 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1445 None, );
1447 let payload = Payload::Peer(peer);
1448 let test_message = Message::Order(MessageKind::new(
1449 Some(uuid),
1450 Some(1),
1451 Some(2),
1452 Action::FiatSentOk,
1453 Some(payload),
1454 ));
1455 assert!(test_message.verify());
1456 let test_message_json = test_message.as_json().unwrap();
1457 let trade_keys =
1459 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1460 .unwrap();
1461 let sig = Message::sign(test_message_json.clone(), &trade_keys);
1462
1463 assert!(Message::verify_signature(
1464 test_message_json,
1465 trade_keys.public_key(),
1466 sig
1467 ));
1468 }
1469
1470 #[test]
1471 fn test_restore_session_message() {
1472 let restore_request_message = Message::Restore(MessageKind::new(
1474 None,
1475 None,
1476 None,
1477 Action::RestoreSession,
1478 None,
1479 ));
1480
1481 assert!(restore_request_message.verify());
1483 assert_eq!(
1484 restore_request_message.inner_action(),
1485 Some(Action::RestoreSession)
1486 );
1487
1488 let message_json = restore_request_message.as_json().unwrap();
1490 let deserialized_message = Message::from_json(&message_json).unwrap();
1491 assert!(deserialized_message.verify());
1492 assert_eq!(
1493 deserialized_message.inner_action(),
1494 Some(Action::RestoreSession)
1495 );
1496
1497 let restored_orders = vec![
1499 crate::message::RestoredOrdersInfo {
1500 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1501 trade_index: 1,
1502 status: "active".to_string(),
1503 },
1504 crate::message::RestoredOrdersInfo {
1505 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1506 trade_index: 2,
1507 status: "success".to_string(),
1508 },
1509 ];
1510
1511 let restored_disputes = vec![
1512 crate::message::RestoredDisputesInfo {
1513 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1514 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1515 trade_index: 1,
1516 status: "initiated".to_string(),
1517 initiator: Some(crate::message::DisputeInitiator::Buyer),
1518 solver_pubkey: None,
1519 },
1520 crate::message::RestoredDisputesInfo {
1521 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1522 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1523 trade_index: 2,
1524 status: "in-progress".to_string(),
1525 initiator: None,
1526 solver_pubkey: Some(
1527 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1528 ),
1529 },
1530 crate::message::RestoredDisputesInfo {
1531 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1532 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1533 trade_index: 3,
1534 status: "initiated".to_string(),
1535 initiator: Some(crate::message::DisputeInitiator::Seller),
1536 solver_pubkey: None,
1537 },
1538 ];
1539
1540 let restore_session_info = crate::message::RestoreSessionInfo {
1541 restore_orders: restored_orders.clone(),
1542 restore_disputes: restored_disputes.clone(),
1543 };
1544
1545 let restore_data_payload = Payload::RestoreData(restore_session_info);
1546 let restore_data_message = Message::Restore(MessageKind::new(
1547 None,
1548 None,
1549 None,
1550 Action::RestoreSession,
1551 Some(restore_data_payload),
1552 ));
1553
1554 assert!(!restore_data_message.verify());
1556
1557 let message_json = restore_data_message.as_json().unwrap();
1559 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1560
1561 if let Message::Restore(kind) = deserialized_restore_message {
1562 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1563 assert_eq!(session_info.restore_disputes.len(), 3);
1564 assert_eq!(
1565 session_info.restore_disputes[0].initiator,
1566 Some(crate::message::DisputeInitiator::Buyer)
1567 );
1568 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1569 assert_eq!(session_info.restore_disputes[1].initiator, None);
1570 assert_eq!(
1571 session_info.restore_disputes[1].solver_pubkey,
1572 Some(
1573 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1574 .to_string()
1575 )
1576 );
1577 assert_eq!(
1578 session_info.restore_disputes[2].initiator,
1579 Some(crate::message::DisputeInitiator::Seller)
1580 );
1581 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1582 } else {
1583 panic!("Expected RestoreData payload");
1584 }
1585 } else {
1586 panic!("Expected Restore message");
1587 }
1588 }
1589
1590 #[test]
1591 fn test_restore_session_message_validation() {
1592 let restore_request_message = Message::Restore(MessageKind::new(
1594 None,
1595 None,
1596 None,
1597 Action::RestoreSession,
1598 None, ));
1600
1601 assert!(restore_request_message.verify());
1603
1604 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1606 let wrong_message = Message::Restore(MessageKind::new(
1607 None,
1608 None,
1609 None,
1610 Action::RestoreSession,
1611 Some(wrong_payload),
1612 ));
1613
1614 assert!(!wrong_message.verify());
1616
1617 let with_id = Message::Restore(MessageKind::new(
1619 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1620 None,
1621 None,
1622 Action::RestoreSession,
1623 None,
1624 ));
1625 assert!(with_id.verify());
1626
1627 let with_request_id = Message::Restore(MessageKind::new(
1628 None,
1629 Some(42),
1630 None,
1631 Action::RestoreSession,
1632 None,
1633 ));
1634 assert!(with_request_id.verify());
1635
1636 let with_trade_index = Message::Restore(MessageKind::new(
1637 None,
1638 None,
1639 Some(7),
1640 Action::RestoreSession,
1641 None,
1642 ));
1643 assert!(with_trade_index.verify());
1644 }
1645
1646 #[test]
1647 fn test_restore_session_message_constructor() {
1648 let restore_request_message = Message::new_restore(None);
1650
1651 assert!(matches!(restore_request_message, Message::Restore(_)));
1652 assert!(restore_request_message.verify());
1653 assert_eq!(
1654 restore_request_message.inner_action(),
1655 Some(Action::RestoreSession)
1656 );
1657
1658 let restore_session_info = crate::message::RestoreSessionInfo {
1660 restore_orders: vec![],
1661 restore_disputes: vec![],
1662 };
1663 let restore_data_message =
1664 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1665
1666 assert!(matches!(restore_data_message, Message::Restore(_)));
1667 assert!(!restore_data_message.verify());
1668 }
1669
1670 #[test]
1671 fn test_last_trade_index_valid_message() {
1672 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1673 let msg = Message::Restore(kind);
1674
1675 assert!(msg.verify());
1676
1677 let json = msg.as_json().unwrap();
1679 let decoded = Message::from_json(&json).unwrap();
1680 assert!(decoded.verify());
1681
1682 let inner = decoded.get_inner_message_kind();
1684 assert_eq!(inner.trade_index(), 7);
1685 assert_eq!(inner.has_trade_index(), (true, 7));
1686 }
1687
1688 #[test]
1689 fn test_last_trade_index_without_id_is_valid() {
1690 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1692 let msg = Message::Restore(kind);
1693 assert!(msg.verify());
1694 }
1695
1696 #[test]
1697 fn test_last_trade_index_with_payload_fails_validation() {
1698 let kind = MessageKind::new(
1700 None,
1701 None,
1702 Some(3),
1703 Action::LastTradeIndex,
1704 Some(Payload::TextMessage("ignored".to_string())),
1705 );
1706 let msg = Message::Restore(kind);
1707 assert!(!msg.verify());
1708 }
1709
1710 #[test]
1711 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1712 use crate::message::BondResolution;
1713
1714 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1715
1716 for action in [Action::AdminSettle, Action::AdminCancel] {
1717 let with_resolution = Message::Order(MessageKind::new(
1718 Some(uuid),
1719 Some(1),
1720 Some(2),
1721 action.clone(),
1722 Some(Payload::BondResolution(BondResolution {
1723 slash_seller: true,
1724 slash_buyer: false,
1725 })),
1726 ));
1727 assert!(
1728 with_resolution.verify(),
1729 "{action:?} + BondResolution should verify"
1730 );
1731
1732 let without_payload = Message::Order(MessageKind::new(
1733 Some(uuid),
1734 Some(1),
1735 Some(2),
1736 action.clone(),
1737 None,
1738 ));
1739 assert!(without_payload.verify(), "{action:?} + None should verify");
1740
1741 let wrong = Message::Order(MessageKind::new(
1743 Some(uuid),
1744 Some(1),
1745 Some(2),
1746 action.clone(),
1747 Some(Payload::TextMessage("nope".to_string())),
1748 ));
1749 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1750
1751 let no_id = Message::Order(MessageKind::new(
1753 None,
1754 Some(1),
1755 Some(2),
1756 action,
1757 Some(Payload::BondResolution(BondResolution {
1758 slash_seller: false,
1759 slash_buyer: false,
1760 })),
1761 ));
1762 assert!(!no_id.verify(), "admin action without id must be rejected");
1763 }
1764 }
1765
1766 #[test]
1767 fn test_bond_resolution_rejected_on_non_admin_actions() {
1768 use crate::message::BondResolution;
1769
1770 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1771 let payload = Payload::BondResolution(BondResolution {
1772 slash_seller: true,
1773 slash_buyer: true,
1774 });
1775
1776 for action in [
1780 Action::NewOrder,
1781 Action::TakeSell,
1782 Action::TakeBuy,
1783 Action::PayInvoice,
1784 Action::PayBondInvoice,
1785 Action::FiatSent,
1786 Action::FiatSentOk,
1787 Action::Release,
1788 Action::Released,
1789 Action::Cancel,
1790 Action::Canceled,
1791 Action::CooperativeCancelInitiatedByYou,
1792 Action::CooperativeCancelInitiatedByPeer,
1793 Action::DisputeInitiatedByYou,
1794 Action::DisputeInitiatedByPeer,
1795 Action::CooperativeCancelAccepted,
1796 Action::BuyerInvoiceAccepted,
1797 Action::BondInvoiceAccepted,
1798 Action::PurchaseCompleted,
1799 Action::BondPayoutCompleted,
1800 Action::BondSlashed,
1801 Action::HoldInvoicePaymentAccepted,
1802 Action::HoldInvoicePaymentSettled,
1803 Action::HoldInvoicePaymentCanceled,
1804 Action::WaitingSellerToPay,
1805 Action::WaitingBuyerInvoice,
1806 Action::AddInvoice,
1807 Action::AddBondInvoice,
1808 Action::BuyerTookOrder,
1809 Action::Rate,
1810 Action::RateUser,
1811 Action::RateReceived,
1812 Action::CantDo,
1813 Action::Dispute,
1814 Action::AdminCanceled,
1815 Action::AdminSettled,
1816 Action::AdminAddSolver,
1817 Action::AdminTakeDispute,
1818 Action::AdminTookDispute,
1819 Action::PaymentFailed,
1820 Action::InvoiceUpdated,
1821 Action::SendDm,
1822 Action::TradePubkey,
1823 Action::RestoreSession,
1824 Action::LastTradeIndex,
1825 Action::Orders,
1826 ] {
1827 let msg = Message::Order(MessageKind::new(
1828 Some(uuid),
1829 Some(1),
1830 Some(2),
1831 action.clone(),
1832 Some(payload.clone()),
1833 ));
1834 assert!(
1835 !msg.verify(),
1836 "{action:?} must reject BondResolution payload"
1837 );
1838 }
1839 }
1840
1841 #[test]
1842 fn test_bond_resolution_wire_format() {
1843 use crate::message::BondResolution;
1844
1845 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1846 let msg = Message::Order(MessageKind::new(
1847 Some(uuid),
1848 None,
1849 None,
1850 Action::AdminCancel,
1851 Some(Payload::BondResolution(BondResolution {
1852 slash_seller: true,
1853 slash_buyer: false,
1854 })),
1855 ));
1856
1857 let json = msg.as_json().unwrap();
1858 assert!(
1860 json.contains("\"bond_resolution\""),
1861 "expected snake_case discriminator, got: {json}"
1862 );
1863 assert!(json.contains("\"slash_seller\":true"));
1864 assert!(json.contains("\"slash_buyer\":false"));
1865
1866 let decoded = Message::from_json(&json).unwrap();
1868 assert!(decoded.verify());
1869 if let Message::Order(kind) = decoded {
1870 match kind.payload {
1871 Some(Payload::BondResolution(b)) => {
1872 assert!(b.slash_seller);
1873 assert!(!b.slash_buyer);
1874 }
1875 other => panic!("expected BondResolution payload, got {other:?}"),
1876 }
1877 } else {
1878 panic!("expected Order message");
1879 }
1880 }
1881
1882 #[test]
1883 fn test_bond_resolution_legacy_null_payload() {
1884 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1888 let json = format!(
1889 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
1890 );
1891 let msg = Message::from_json(&json).unwrap();
1892 assert!(msg.verify());
1893 }
1894
1895 #[test]
1896 fn test_pay_bond_invoice_wire_format_and_verify() {
1897 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1898 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1899
1900 let msg = Message::Order(MessageKind::new(
1901 Some(uuid),
1902 Some(1),
1903 Some(2),
1904 Action::PayBondInvoice,
1905 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1906 ));
1907 assert!(msg.verify());
1908
1909 let json = msg.as_json().unwrap();
1911 assert!(
1912 json.contains("\"action\":\"pay-bond-invoice\""),
1913 "expected kebab-case discriminator, got: {json}"
1914 );
1915
1916 let decoded = Message::from_json(&json).unwrap();
1918 assert!(decoded.verify());
1919 assert!(matches!(
1920 decoded.inner_action(),
1921 Some(Action::PayBondInvoice)
1922 ));
1923
1924 let no_id = Message::Order(MessageKind::new(
1926 None,
1927 Some(1),
1928 Some(2),
1929 Action::PayBondInvoice,
1930 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1931 ));
1932 assert!(!no_id.verify());
1933
1934 let wrong_payload = Message::Order(MessageKind::new(
1936 Some(uuid),
1937 Some(1),
1938 Some(2),
1939 Action::PayBondInvoice,
1940 Some(Payload::TextMessage("nope".to_string())),
1941 ));
1942 assert!(!wrong_payload.verify());
1943
1944 let no_payload = Message::Order(MessageKind::new(
1946 Some(uuid),
1947 Some(1),
1948 Some(2),
1949 Action::PayBondInvoice,
1950 None,
1951 ));
1952 assert!(!no_payload.verify());
1953 }
1954
1955 #[test]
1956 fn test_add_bond_invoice_wire_format_and_verify() {
1957 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1958 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1959
1960 let msg = Message::Order(MessageKind::new(
1961 Some(uuid),
1962 Some(1),
1963 Some(2),
1964 Action::AddBondInvoice,
1965 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1966 ));
1967 assert!(msg.verify());
1968
1969 let json = msg.as_json().unwrap();
1971 assert!(
1972 json.contains("\"action\":\"add-bond-invoice\""),
1973 "expected kebab-case discriminator, got: {json}"
1974 );
1975
1976 let decoded = Message::from_json(&json).unwrap();
1978 assert!(decoded.verify());
1979 assert!(matches!(
1980 decoded.inner_action(),
1981 Some(Action::AddBondInvoice)
1982 ));
1983
1984 let no_id = Message::Order(MessageKind::new(
1986 None,
1987 Some(1),
1988 Some(2),
1989 Action::AddBondInvoice,
1990 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1991 ));
1992 assert!(!no_id.verify());
1993
1994 let wrong_payload = Message::Order(MessageKind::new(
1996 Some(uuid),
1997 Some(1),
1998 Some(2),
1999 Action::AddBondInvoice,
2000 Some(Payload::TextMessage("nope".to_string())),
2001 ));
2002 assert!(!wrong_payload.verify());
2003
2004 let no_payload = Message::Order(MessageKind::new(
2006 Some(uuid),
2007 Some(1),
2008 Some(2),
2009 Action::AddBondInvoice,
2010 None,
2011 ));
2012 assert!(!no_payload.verify());
2013
2014 if let Message::Order(kind) = &msg {
2016 assert_eq!(kind.get_payment_request(), Some(bolt11));
2017 } else {
2018 panic!("expected Message::Order");
2019 }
2020 }
2021
2022 #[test]
2023 fn test_restored_dispute_helper_serialization_roundtrip() {
2024 use crate::message::RestoredDisputeHelper;
2025
2026 let helper = RestoredDisputeHelper {
2027 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
2028 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
2029 dispute_status: "initiated".to_string(),
2030 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
2031 master_seller_pubkey: Some("npub1sellerkey".to_string()),
2032 trade_index_buyer: Some(1),
2033 trade_index_seller: Some(2),
2034 buyer_dispute: true,
2035 seller_dispute: false,
2036 solver_pubkey: None,
2037 };
2038
2039 let json = serde_json::to_string(&helper).unwrap();
2040 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
2041
2042 assert_eq!(deserialized.dispute_id, helper.dispute_id);
2043 assert_eq!(deserialized.order_id, helper.order_id);
2044 assert_eq!(deserialized.dispute_status, helper.dispute_status);
2045 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
2046 assert_eq!(
2047 deserialized.master_seller_pubkey,
2048 helper.master_seller_pubkey
2049 );
2050 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
2051 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
2052 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
2053 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
2054 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
2055
2056 let helper_seller_dispute = RestoredDisputeHelper {
2057 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
2058 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
2059 dispute_status: "in-progress".to_string(),
2060 master_buyer_pubkey: None,
2061 master_seller_pubkey: None,
2062 trade_index_buyer: None,
2063 trade_index_seller: None,
2064 buyer_dispute: false,
2065 seller_dispute: true,
2066 solver_pubkey: Some(
2067 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
2068 ),
2069 };
2070
2071 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
2072 let deserialized_seller: RestoredDisputeHelper =
2073 serde_json::from_str(&json_seller).unwrap();
2074
2075 assert_eq!(
2076 deserialized_seller.dispute_id,
2077 helper_seller_dispute.dispute_id
2078 );
2079 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
2080 assert_eq!(
2081 deserialized_seller.dispute_status,
2082 helper_seller_dispute.dispute_status
2083 );
2084 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
2085 assert_eq!(deserialized_seller.master_seller_pubkey, None);
2086 assert_eq!(deserialized_seller.trade_index_buyer, None);
2087 assert_eq!(deserialized_seller.trade_index_seller, None);
2088 assert!(!deserialized_seller.buyer_dispute);
2089 assert!(deserialized_seller.seller_dispute);
2090 assert_eq!(
2091 deserialized_seller.solver_pubkey,
2092 helper_seller_dispute.solver_pubkey
2093 );
2094 }
2095
2096 fn sample_lock_proof() -> CashuLockProof {
2097 CashuLockProof::new(
2098 "cashuAeyJ0b2tlbiI6dGVzdA".to_string(),
2099 "https://mint.example".to_string(),
2100 "02b_buyer".to_string(),
2101 "02s_seller".to_string(),
2102 "02m_mostro".to_string(),
2103 )
2104 }
2105
2106 #[test]
2107 fn test_cashu_lock_proof_json_round_trip() {
2108 let proof = sample_lock_proof();
2109 let json = proof.as_json().unwrap();
2110 let back = CashuLockProof::from_json(&json).unwrap();
2111 assert_eq!(back, proof);
2112 }
2113
2114 #[test]
2115 fn test_cashu_lock_proof_fee_token_round_trip() {
2116 let proof = sample_lock_proof().with_fee_token("cashuAfee".to_string());
2117 let json = proof.as_json().unwrap();
2118 assert!(json.contains("fee_token"));
2119 let back = CashuLockProof::from_json(&json).unwrap();
2120 assert_eq!(back, proof);
2121 assert_eq!(back.fee_token.as_deref(), Some("cashuAfee"));
2122 }
2123
2124 #[test]
2125 fn test_cashu_lock_proof_without_fee_token_is_omitted_and_defaults_to_none() {
2126 let proof = sample_lock_proof();
2129 assert_eq!(proof.fee_token, None);
2130 let json = proof.as_json().unwrap();
2131 assert!(!json.contains("fee_token"));
2132 let back = CashuLockProof::from_json(&json).unwrap();
2133 assert_eq!(back.fee_token, None);
2134 }
2135
2136 #[test]
2137 fn test_add_cashu_escrow_verifies_with_lock_proof() {
2138 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2139 let payload = Payload::CashuLockProof(sample_lock_proof());
2140 let kind = MessageKind::new(
2141 Some(order_id),
2142 None,
2143 None,
2144 Action::AddCashuEscrow,
2145 Some(payload),
2146 );
2147 assert!(
2148 kind.verify(),
2149 "CashuLockProof must verify on AddCashuEscrow"
2150 );
2151
2152 let json = Message::Order(kind).as_json().unwrap();
2155 assert!(json.contains("cashu_lock_proof"));
2156 assert!(Message::from_json(&json).unwrap().verify());
2157 }
2158
2159 #[test]
2160 fn test_add_cashu_escrow_requires_id_and_right_payload() {
2161 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2162
2163 let no_id = MessageKind::new(
2165 None,
2166 None,
2167 None,
2168 Action::AddCashuEscrow,
2169 Some(Payload::CashuLockProof(sample_lock_proof())),
2170 );
2171 assert!(
2172 !no_id.verify(),
2173 "AddCashuEscrow without id must be rejected"
2174 );
2175
2176 let wrong_payload = MessageKind::new(
2178 Some(order_id),
2179 None,
2180 None,
2181 Action::AddCashuEscrow,
2182 Some(Payload::TextMessage("not a lock proof".to_string())),
2183 );
2184 assert!(
2185 !wrong_payload.verify(),
2186 "AddCashuEscrow with non-lock-proof payload must be rejected"
2187 );
2188 }
2189
2190 #[test]
2191 fn test_cashu_pm_signature_verifies_with_signatures() {
2192 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2193 let kind = MessageKind::new(
2194 Some(order_id),
2195 None,
2196 None,
2197 Action::CashuPmSignature,
2198 Some(Payload::CashuSignatures(vec![
2199 CashuProofSignature::new("secret-0".to_string(), "deadbeef".to_string()),
2200 CashuProofSignature::new("secret-1".to_string(), "c0ffee".to_string()),
2201 ])),
2202 );
2203 assert!(
2204 kind.verify(),
2205 "CashuSignatures must verify on CashuPmSignature"
2206 );
2207
2208 let json = Message::Order(kind).as_json().unwrap();
2209 assert!(json.contains("cashu_signatures"));
2210 assert!(Message::from_json(&json).unwrap().verify());
2211
2212 let wrong = MessageKind::new(Some(order_id), None, None, Action::CashuPmSignature, None);
2214 assert!(
2215 !wrong.verify(),
2216 "CashuPmSignature without a signature payload must be rejected"
2217 );
2218
2219 let empty = MessageKind::new(
2222 Some(order_id),
2223 None,
2224 None,
2225 Action::CashuPmSignature,
2226 Some(Payload::CashuSignatures(vec![])),
2227 );
2228 assert!(
2229 !empty.verify(),
2230 "CashuPmSignature with an empty signature set must be rejected"
2231 );
2232 }
2233
2234 #[test]
2235 fn test_cashu_escrow_locked_is_informational() {
2236 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2237
2238 let ok = MessageKind::new(Some(order_id), None, None, Action::CashuEscrowLocked, None);
2240 assert!(ok.verify(), "CashuEscrowLocked with id must verify");
2241
2242 let no_id = MessageKind::new(None, None, None, Action::CashuEscrowLocked, None);
2244 assert!(
2245 !no_id.verify(),
2246 "CashuEscrowLocked without id must be rejected"
2247 );
2248 }
2249}