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 DeclarePayer,
210 PayerDeclared,
214 PaymentHistory,
219 ExportReputation,
224 ReputationExported,
228 ImportReputation,
232 ReputationImported,
236}
237
238impl fmt::Display for Action {
239 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
240 write!(f, "{self:?}")
241 }
242}
243
244#[derive(Debug, Clone, Deserialize, Serialize)]
251#[serde(rename_all = "kebab-case")]
252pub enum Message {
253 Order(MessageKind),
255 Dispute(MessageKind),
257 CantDo(MessageKind),
259 Rate(MessageKind),
261 Dm(MessageKind),
263 Restore(MessageKind),
265}
266
267impl Message {
268 pub fn new_order(
271 id: Option<Uuid>,
272 request_id: Option<u64>,
273 trade_index: Option<i64>,
274 action: Action,
275 payload: Option<Payload>,
276 ) -> Self {
277 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
278 Self::Order(kind)
279 }
280
281 pub fn new_dispute(
284 id: Option<Uuid>,
285 request_id: Option<u64>,
286 trade_index: Option<i64>,
287 action: Action,
288 payload: Option<Payload>,
289 ) -> Self {
290 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
291
292 Self::Dispute(kind)
293 }
294
295 pub fn new_restore(payload: Option<Payload>) -> Self {
300 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
301 Self::Restore(kind)
302 }
303
304 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
307 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
308
309 Self::CantDo(kind)
310 }
311
312 pub fn new_dm(
314 id: Option<Uuid>,
315 request_id: Option<u64>,
316 action: Action,
317 payload: Option<Payload>,
318 ) -> Self {
319 let kind = MessageKind::new(id, request_id, None, action, payload);
320
321 Self::Dm(kind)
322 }
323
324 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
326 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
327 }
328
329 pub fn as_json(&self) -> Result<String, ServiceError> {
331 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
332 }
333
334 pub fn get_inner_message_kind(&self) -> &MessageKind {
336 match self {
337 Message::Dispute(k)
338 | Message::Order(k)
339 | Message::CantDo(k)
340 | Message::Rate(k)
341 | Message::Dm(k)
342 | Message::Restore(k) => k,
343 }
344 }
345
346 pub fn inner_action(&self) -> Option<Action> {
351 match self {
352 Message::Dispute(a)
353 | Message::Order(a)
354 | Message::CantDo(a)
355 | Message::Rate(a)
356 | Message::Dm(a)
357 | Message::Restore(a) => Some(a.get_action()),
358 }
359 }
360
361 pub fn verify(&self) -> bool {
364 match self {
365 Message::Order(m)
366 | Message::Dispute(m)
367 | Message::CantDo(m)
368 | Message::Rate(m)
369 | Message::Dm(m)
370 | Message::Restore(m) => m.verify(),
371 }
372 }
373
374 pub fn sign(message: String, keys: &Keys) -> Signature {
386 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
387 keys.sign_schnorr(hash.to_byte_array())
388 }
389
390 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
400 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
401 let hash = hash.to_byte_array();
402
403 let secp = Secp256k1::verification_only();
404 if let Ok(xonlykey) = pubkey.xonly() {
405 let sig = schnorr::Signature::from_byte_array(*sig.as_bytes());
406 xonlykey.verify(&secp, &hash, &sig).is_ok()
407 } else {
408 false
409 }
410 }
411}
412
413#[derive(Debug, Clone, Deserialize, Serialize)]
420pub struct MessageKind {
421 pub version: u8,
424 pub request_id: Option<u64>,
427 pub trade_index: Option<i64>,
430 #[serde(skip_serializing_if = "Option::is_none")]
433 pub id: Option<Uuid>,
434 pub action: Action,
436 pub payload: Option<Payload>,
439}
440
441type Amount = i64;
443
444#[derive(Debug, Deserialize, Serialize, Clone)]
449pub struct PaymentFailedInfo {
450 pub payment_attempts: u32,
452 pub payment_retries_interval: u32,
454}
455
456#[cfg_attr(feature = "sqlx", derive(FromRow))]
461#[derive(Debug, Deserialize, Serialize, Clone)]
462pub struct RestoredOrderHelper {
463 pub id: Uuid,
465 pub status: String,
467 pub master_buyer_pubkey: Option<String>,
469 pub master_seller_pubkey: Option<String>,
471 pub trade_index_buyer: Option<i64>,
473 pub trade_index_seller: Option<i64>,
475}
476
477#[cfg_attr(feature = "sqlx", derive(FromRow))]
482#[derive(Debug, Deserialize, Serialize, Clone)]
483pub struct RestoredDisputeHelper {
484 pub dispute_id: Uuid,
486 pub order_id: Uuid,
488 pub dispute_status: String,
490 pub master_buyer_pubkey: Option<String>,
492 pub master_seller_pubkey: Option<String>,
494 pub trade_index_buyer: Option<i64>,
496 pub trade_index_seller: Option<i64>,
498 pub buyer_dispute: bool,
502 pub seller_dispute: bool,
506 pub solver_pubkey: Option<String>,
509}
510
511#[cfg_attr(feature = "sqlx", derive(FromRow))]
513#[derive(Debug, Deserialize, Serialize, Clone)]
514pub struct RestoredOrdersInfo {
515 pub order_id: Uuid,
517 pub trade_index: i64,
519 pub status: String,
521 #[serde(default)]
535 pub counterparty_trade_pubkey: Option<String>,
536}
537
538#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
540#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
541#[serde(rename_all = "lowercase")]
542#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
543pub enum DisputeInitiator {
544 Buyer,
546 Seller,
548}
549
550#[cfg_attr(feature = "sqlx", derive(FromRow))]
552#[derive(Debug, Deserialize, Serialize, Clone)]
553pub struct RestoredDisputesInfo {
554 pub dispute_id: Uuid,
556 pub order_id: Uuid,
558 pub trade_index: i64,
560 pub status: String,
562 pub initiator: Option<DisputeInitiator>,
565 pub solver_pubkey: Option<String>,
568}
569
570#[derive(Debug, Deserialize, Serialize, Clone, Default)]
575pub struct RestoreSessionInfo {
576 #[serde(rename = "orders")]
578 pub restore_orders: Vec<RestoredOrdersInfo>,
579 #[serde(rename = "disputes")]
581 pub restore_disputes: Vec<RestoredDisputesInfo>,
582}
583
584#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
592pub struct BondResolution {
593 pub slash_seller: bool,
595 pub slash_buyer: bool,
597}
598
599#[derive(Debug, Deserialize, Serialize, Clone)]
614pub struct BondPayoutRequest {
615 pub order: SmallOrder,
619 pub slashed_at: i64,
624}
625
626#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
631pub struct ReputationExportRequest {
632 pub destination: String,
634 #[serde(default)]
639 pub rebind: Option<String>,
640}
641
642#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
651pub struct CashuLockProof {
652 pub token: String,
654 pub mint_url: String,
657 pub buyer_pubkey: String,
659 pub seller_pubkey: String,
661 pub mostro_pubkey: String,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub fee_token: Option<String>,
670}
671
672impl CashuLockProof {
673 pub fn new(
675 token: String,
676 mint_url: String,
677 buyer_pubkey: String,
678 seller_pubkey: String,
679 mostro_pubkey: String,
680 ) -> Self {
681 Self {
682 token,
683 mint_url,
684 buyer_pubkey,
685 seller_pubkey,
686 mostro_pubkey,
687 fee_token: None,
688 }
689 }
690
691 pub fn with_fee_token(mut self, fee_token: String) -> Self {
694 self.fee_token = Some(fee_token);
695 self
696 }
697
698 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
700 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
701 }
702
703 pub fn as_json(&self) -> Result<String, ServiceError> {
705 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
706 }
707}
708
709#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
718pub struct CashuProofSignature {
719 pub secret: String,
723 pub signature: String,
726}
727
728impl CashuProofSignature {
729 pub fn new(secret: String, signature: String) -> Self {
731 Self { secret, signature }
732 }
733}
734
735#[derive(Debug, Deserialize, Serialize, Clone)]
741#[serde(rename_all = "snake_case")]
742pub enum Payload {
743 Order(SmallOrder),
745 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
753 TextMessage(String),
755 Peer(Peer),
757 RatingUser(u8),
759 Amount(Amount),
761 Dispute(Uuid, Option<SolverDisputeInfo>),
764 CantDo(Option<CantDoReason>),
766 NextTrade(String, u32),
769 PaymentFailed(PaymentFailedInfo),
771 RestoreData(RestoreSessionInfo),
773 Ids(Vec<Uuid>),
775 Orders(Vec<SmallOrder>),
777 BondResolution(BondResolution),
780 BondPayoutRequest(BondPayoutRequest),
786 CashuLockProof(CashuLockProof),
789 CashuSignatures(Vec<CashuProofSignature>),
795 PayerDeclaration(PayerDeclaration),
799 PaymentHistory(PaymentHistory),
803 ReputationExportRequest(ReputationExportRequest),
806 ReputationAttestation(String),
812}
813
814#[allow(dead_code)]
815impl MessageKind {
816 pub fn new(
819 id: Option<Uuid>,
820 request_id: Option<u64>,
821 trade_index: Option<i64>,
822 action: Action,
823 payload: Option<Payload>,
824 ) -> Self {
825 Self {
826 version: PROTOCOL_VER,
827 request_id,
828 trade_index,
829 id,
830 action,
831 payload,
832 }
833 }
834 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
836 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
837 }
838 pub fn as_json(&self) -> Result<String, ServiceError> {
840 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
841 }
842
843 pub fn get_action(&self) -> Action {
845 self.action.clone()
846 }
847
848 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
855 match &self.payload {
856 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
857 None => Ok(None),
858 _ => Err(ServiceError::InvalidPayload),
859 }
860 }
861
862 pub fn get_rating(&self) -> Result<u8, ServiceError> {
870 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
871 if !(MIN_RATING..=MAX_RATING).contains(&v) {
872 return Err(ServiceError::InvalidRatingValue);
873 }
874 Ok(v)
875 } else {
876 Err(ServiceError::InvalidRating)
877 }
878 }
879
880 pub fn verify(&self) -> bool {
887 match &self.action {
888 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
889 Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
890 if self.id.is_none() {
891 return false;
892 }
893 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
894 }
895 Action::AddBondInvoice => {
896 if self.id.is_none() {
897 return false;
898 }
899 matches!(
905 &self.payload,
906 Some(Payload::BondPayoutRequest(_)) | Some(Payload::PaymentRequest(_, _, _))
907 )
908 }
909 Action::AdminSettle | Action::AdminCancel => {
910 if self.id.is_none() {
911 return false;
912 }
913 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
914 }
915 Action::AddCashuEscrow => {
916 if self.id.is_none() {
917 return false;
918 }
919 matches!(&self.payload, Some(Payload::CashuLockProof(_)))
920 }
921 Action::CashuPmSignature => {
922 if self.id.is_none() {
923 return false;
924 }
925 matches!(&self.payload, Some(Payload::CashuSignatures(sigs)) if !sigs.is_empty())
926 }
927 Action::DeclarePayer | Action::PayerDeclared => {
928 if self.id.is_none() {
929 return false;
930 }
931 matches!(&self.payload, Some(Payload::PayerDeclaration(_)))
932 }
933 Action::PaymentHistory => {
934 if self.id.is_none() {
935 return false;
936 }
937 matches!(&self.payload, None | Some(Payload::PaymentHistory(_)))
940 }
941 Action::TakeSell
942 | Action::TakeBuy
943 | Action::FiatSent
944 | Action::FiatSentOk
945 | Action::Release
946 | Action::Released
947 | Action::Dispute
948 | Action::AdminCanceled
949 | Action::AdminSettled
950 | Action::Rate
951 | Action::RateReceived
952 | Action::AdminTakeDispute
953 | Action::AdminTookDispute
954 | Action::DisputeInitiatedByYou
955 | Action::DisputeInitiatedByPeer
956 | Action::WaitingBuyerInvoice
957 | Action::PurchaseCompleted
958 | Action::BondPayoutCompleted
959 | Action::BondSlashed
960 | Action::HoldInvoicePaymentAccepted
961 | Action::HoldInvoicePaymentSettled
962 | Action::HoldInvoicePaymentCanceled
963 | Action::WaitingSellerToPay
964 | Action::BuyerTookOrder
965 | Action::BuyerInvoiceAccepted
966 | Action::BondInvoiceAccepted
967 | Action::CooperativeCancelInitiatedByYou
968 | Action::CooperativeCancelInitiatedByPeer
969 | Action::CooperativeCancelAccepted
970 | Action::Cancel
971 | Action::InvoiceUpdated
972 | Action::AdminAddSolver
973 | Action::SendDm
974 | Action::TradePubkey
975 | Action::CashuEscrowLocked
976 | Action::Canceled => {
977 if self.id.is_none() {
978 return false;
979 }
980 !matches!(
982 &self.payload,
983 Some(Payload::BondResolution(_))
984 | Some(Payload::BondPayoutRequest(_))
985 | Some(Payload::PayerDeclaration(_))
986 | Some(Payload::PaymentHistory(_))
987 )
988 }
989 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
990 Action::PaymentFailed => {
991 if self.id.is_none() {
992 return false;
993 }
994 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
995 }
996 Action::RateUser => {
997 matches!(&self.payload, Some(Payload::RatingUser(_)))
998 }
999 Action::CantDo => {
1000 matches!(&self.payload, Some(Payload::CantDo(_)))
1001 }
1002 Action::Orders => {
1003 matches!(
1004 &self.payload,
1005 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
1006 )
1007 }
1008 Action::ExportReputation => {
1009 self.id.is_none()
1010 && matches!(&self.payload, Some(Payload::ReputationExportRequest(_)))
1011 }
1012 Action::ReputationExported | Action::ImportReputation => {
1013 self.id.is_none()
1014 && matches!(&self.payload, Some(Payload::ReputationAttestation(_)))
1015 }
1016 Action::ReputationImported => self.id.is_none() && self.payload.is_none(),
1017 }
1018 }
1019
1020 pub fn get_order(&self) -> Option<&SmallOrder> {
1025 if self.action != Action::NewOrder {
1026 return None;
1027 }
1028 match &self.payload {
1029 Some(Payload::Order(o)) => Some(o),
1030 _ => None,
1031 }
1032 }
1033
1034 pub fn get_payment_request(&self) -> Option<String> {
1041 if self.action != Action::TakeSell
1042 && self.action != Action::AddInvoice
1043 && self.action != Action::AddBondInvoice
1044 && self.action != Action::NewOrder
1045 {
1046 return None;
1047 }
1048 match &self.payload {
1049 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
1050 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
1051 _ => None,
1052 }
1053 }
1054
1055 pub fn get_amount(&self) -> Option<Amount> {
1059 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
1060 return None;
1061 }
1062 match &self.payload {
1063 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
1064 Some(Payload::Amount(amount)) => Some(*amount),
1065 _ => None,
1066 }
1067 }
1068
1069 pub fn get_payload(&self) -> Option<&Payload> {
1071 self.payload.as_ref()
1072 }
1073
1074 pub fn has_trade_index(&self) -> (bool, i64) {
1077 if let Some(index) = self.trade_index {
1078 return (true, index);
1079 }
1080 (false, 0)
1081 }
1082
1083 pub fn trade_index(&self) -> i64 {
1085 if let Some(index) = self.trade_index {
1086 return index;
1087 }
1088 0
1089 }
1090}
1091
1092#[cfg(test)]
1093mod test {
1094 use crate::message::{
1095 Action, BondPayoutRequest, CashuLockProof, CashuProofSignature, Message, MessageKind,
1096 Payload, Peer,
1097 };
1098 use crate::order::SmallOrder;
1099 use crate::payer::{payment_hash, BuyerMode, PayerDeclaration, PaymentHistory};
1100 use crate::user::UserInfo;
1101 use nostr_sdk::prelude::Keys;
1102 use uuid::uuid;
1103
1104 #[test]
1105 fn test_peer_with_reputation() {
1106 let reputation = UserInfo {
1108 rating: 4.5,
1109 reviews: 10,
1110 operating_days: 30,
1111 since: None,
1112 };
1113 let peer = Peer::new(
1114 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1115 Some(reputation.clone()),
1116 );
1117
1118 assert_eq!(
1120 peer.pubkey,
1121 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1122 );
1123 assert!(peer.reputation.is_some());
1124 let peer_reputation = peer.reputation.clone().unwrap();
1125 assert_eq!(peer_reputation.rating, 4.5);
1126 assert_eq!(peer_reputation.reviews, 10);
1127 assert_eq!(peer_reputation.operating_days, 30);
1128
1129 let json = peer.as_json().unwrap();
1131 let deserialized_peer = Peer::from_json(&json).unwrap();
1132 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1133 assert!(deserialized_peer.reputation.is_some());
1134 let deserialized_reputation = deserialized_peer.reputation.unwrap();
1135 assert_eq!(deserialized_reputation.rating, 4.5);
1136 assert_eq!(deserialized_reputation.reviews, 10);
1137 assert_eq!(deserialized_reputation.operating_days, 30);
1138 }
1139
1140 #[test]
1141 fn test_peer_without_reputation() {
1142 let peer = Peer::new(
1144 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1145 None,
1146 );
1147
1148 assert_eq!(
1150 peer.pubkey,
1151 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1152 );
1153 assert!(peer.reputation.is_none());
1154
1155 let json = peer.as_json().unwrap();
1157 let deserialized_peer = Peer::from_json(&json).unwrap();
1158 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1159 assert!(deserialized_peer.reputation.is_none());
1160 }
1161
1162 #[test]
1163 fn test_peer_in_message() {
1164 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1165
1166 let reputation = UserInfo {
1168 rating: 4.5,
1169 reviews: 10,
1170 operating_days: 30,
1171 since: None,
1172 };
1173 let peer_with_reputation = Peer::new(
1174 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1175 Some(reputation),
1176 );
1177 let payload_with_reputation = Payload::Peer(peer_with_reputation);
1178 let message_with_reputation = Message::Order(MessageKind::new(
1179 Some(uuid),
1180 Some(1),
1181 Some(2),
1182 Action::FiatSentOk,
1183 Some(payload_with_reputation),
1184 ));
1185
1186 assert!(message_with_reputation.verify());
1188 let message_json = message_with_reputation.as_json().unwrap();
1189 let deserialized_message = Message::from_json(&message_json).unwrap();
1190 assert!(deserialized_message.verify());
1191
1192 let peer_without_reputation = Peer::new(
1194 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1195 None,
1196 );
1197 let payload_without_reputation = Payload::Peer(peer_without_reputation);
1198 let message_without_reputation = Message::Order(MessageKind::new(
1199 Some(uuid),
1200 Some(1),
1201 Some(2),
1202 Action::FiatSentOk,
1203 Some(payload_without_reputation),
1204 ));
1205
1206 assert!(message_without_reputation.verify());
1208 let message_json = message_without_reputation.as_json().unwrap();
1209 let deserialized_message = Message::from_json(&message_json).unwrap();
1210 assert!(deserialized_message.verify());
1211 }
1212
1213 #[test]
1214 fn test_bond_payout_request_payload_verifies_on_add_bond_invoice() {
1215 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1216 let order = SmallOrder {
1217 id: Some(order_id),
1218 kind: None,
1219 status: None,
1220 amount: 500,
1221 fiat_code: "USD".to_string(),
1222 min_amount: None,
1223 max_amount: None,
1224 fiat_amount: 0,
1225 payment_method: "lightning".to_string(),
1226 premium: 0,
1227 buyer_trade_pubkey: None,
1228 seller_trade_pubkey: None,
1229 buyer_invoice: None,
1230 created_at: None,
1231 expires_at: None,
1232 };
1233 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1234 order,
1235 slashed_at: 1_734_000_000,
1236 });
1237 let kind = MessageKind::new(
1238 Some(order_id),
1239 None,
1240 None,
1241 Action::AddBondInvoice,
1242 Some(payload),
1243 );
1244 assert!(
1245 kind.verify(),
1246 "BondPayoutRequest must verify on AddBondInvoice"
1247 );
1248
1249 let m = Message::Order(kind);
1252 let json = m.as_json().unwrap();
1253 assert!(json.contains("bond_payout_request"));
1254 let back = Message::from_json(&json).unwrap();
1255 assert!(back.verify());
1256 }
1257
1258 #[test]
1259 fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1260 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1263 let order = SmallOrder {
1264 id: Some(order_id),
1265 kind: None,
1266 status: None,
1267 amount: 500,
1268 fiat_code: "USD".to_string(),
1269 min_amount: None,
1270 max_amount: None,
1271 fiat_amount: 0,
1272 payment_method: "lightning".to_string(),
1273 premium: 0,
1274 buyer_trade_pubkey: None,
1275 seller_trade_pubkey: None,
1276 buyer_invoice: None,
1277 created_at: None,
1278 expires_at: None,
1279 };
1280
1281 let _exhaustive: fn(Action) = |a| match a {
1286 Action::AddBondInvoice => {}
1287 Action::NewOrder
1288 | Action::TakeSell
1289 | Action::TakeBuy
1290 | Action::PayInvoice
1291 | Action::PayBondInvoice
1292 | Action::FiatSent
1293 | Action::FiatSentOk
1294 | Action::Release
1295 | Action::Released
1296 | Action::Cancel
1297 | Action::Canceled
1298 | Action::CooperativeCancelInitiatedByYou
1299 | Action::CooperativeCancelInitiatedByPeer
1300 | Action::DisputeInitiatedByYou
1301 | Action::DisputeInitiatedByPeer
1302 | Action::CooperativeCancelAccepted
1303 | Action::BuyerInvoiceAccepted
1304 | Action::BondInvoiceAccepted
1305 | Action::PurchaseCompleted
1306 | Action::BondPayoutCompleted
1307 | Action::BondSlashed
1308 | Action::HoldInvoicePaymentAccepted
1309 | Action::HoldInvoicePaymentSettled
1310 | Action::HoldInvoicePaymentCanceled
1311 | Action::WaitingSellerToPay
1312 | Action::WaitingBuyerInvoice
1313 | Action::AddInvoice
1314 | Action::BuyerTookOrder
1315 | Action::Rate
1316 | Action::RateUser
1317 | Action::RateReceived
1318 | Action::CantDo
1319 | Action::Dispute
1320 | Action::AdminCancel
1321 | Action::AdminCanceled
1322 | Action::AdminSettle
1323 | Action::AdminSettled
1324 | Action::AdminAddSolver
1325 | Action::AdminTakeDispute
1326 | Action::AdminTookDispute
1327 | Action::PaymentFailed
1328 | Action::InvoiceUpdated
1329 | Action::SendDm
1330 | Action::TradePubkey
1331 | Action::RestoreSession
1332 | Action::LastTradeIndex
1333 | Action::AddCashuEscrow
1334 | Action::CashuEscrowLocked
1335 | Action::CashuPmSignature
1336 | Action::DeclarePayer
1337 | Action::PayerDeclared
1338 | Action::PaymentHistory
1339 | Action::ExportReputation
1340 | Action::ReputationExported
1341 | Action::ImportReputation
1342 | Action::ReputationImported
1343 | Action::Orders => {}
1344 };
1345
1346 let other_actions: &[Action] = &[
1347 Action::NewOrder,
1348 Action::TakeSell,
1349 Action::TakeBuy,
1350 Action::PayInvoice,
1351 Action::PayBondInvoice,
1352 Action::FiatSent,
1353 Action::FiatSentOk,
1354 Action::Release,
1355 Action::Released,
1356 Action::Cancel,
1357 Action::Canceled,
1358 Action::CooperativeCancelInitiatedByYou,
1359 Action::CooperativeCancelInitiatedByPeer,
1360 Action::DisputeInitiatedByYou,
1361 Action::DisputeInitiatedByPeer,
1362 Action::CooperativeCancelAccepted,
1363 Action::BuyerInvoiceAccepted,
1364 Action::BondInvoiceAccepted,
1365 Action::PurchaseCompleted,
1366 Action::BondPayoutCompleted,
1367 Action::BondSlashed,
1368 Action::HoldInvoicePaymentAccepted,
1369 Action::HoldInvoicePaymentSettled,
1370 Action::HoldInvoicePaymentCanceled,
1371 Action::WaitingSellerToPay,
1372 Action::WaitingBuyerInvoice,
1373 Action::AddInvoice,
1374 Action::BuyerTookOrder,
1375 Action::Rate,
1376 Action::RateUser,
1377 Action::RateReceived,
1378 Action::CantDo,
1379 Action::Dispute,
1380 Action::AdminCancel,
1381 Action::AdminCanceled,
1382 Action::AdminSettle,
1383 Action::AdminSettled,
1384 Action::AdminAddSolver,
1385 Action::AdminTakeDispute,
1386 Action::AdminTookDispute,
1387 Action::PaymentFailed,
1388 Action::InvoiceUpdated,
1389 Action::SendDm,
1390 Action::TradePubkey,
1391 Action::RestoreSession,
1392 Action::LastTradeIndex,
1393 Action::Orders,
1394 Action::AddCashuEscrow,
1395 Action::CashuEscrowLocked,
1396 Action::CashuPmSignature,
1397 Action::DeclarePayer,
1398 Action::PayerDeclared,
1399 Action::PaymentHistory,
1400 Action::ExportReputation,
1401 Action::ReputationExported,
1402 Action::ImportReputation,
1403 Action::ReputationImported,
1404 ];
1405
1406 for action in other_actions {
1407 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1408 order: order.clone(),
1409 slashed_at: 0,
1410 });
1411 let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1412 assert!(
1413 !kind.verify(),
1414 "BondPayoutRequest must be rejected on {action:?}"
1415 );
1416 }
1417 }
1418
1419 #[test]
1420 fn test_payer_payloads_rejected_on_unrelated_actions() {
1421 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1424 let unrelated = [
1425 Action::NewOrder,
1426 Action::TakeSell,
1427 Action::TakeBuy,
1428 Action::PayInvoice,
1429 Action::PayBondInvoice,
1430 Action::FiatSent,
1431 Action::FiatSentOk,
1432 Action::Release,
1433 Action::Released,
1434 Action::Cancel,
1435 Action::Canceled,
1436 Action::CooperativeCancelInitiatedByYou,
1437 Action::CooperativeCancelInitiatedByPeer,
1438 Action::DisputeInitiatedByYou,
1439 Action::DisputeInitiatedByPeer,
1440 Action::CooperativeCancelAccepted,
1441 Action::BuyerInvoiceAccepted,
1442 Action::BondInvoiceAccepted,
1443 Action::PurchaseCompleted,
1444 Action::BondPayoutCompleted,
1445 Action::BondSlashed,
1446 Action::HoldInvoicePaymentAccepted,
1447 Action::HoldInvoicePaymentSettled,
1448 Action::HoldInvoicePaymentCanceled,
1449 Action::WaitingSellerToPay,
1450 Action::WaitingBuyerInvoice,
1451 Action::AddInvoice,
1452 Action::BuyerTookOrder,
1453 Action::Rate,
1454 Action::RateUser,
1455 Action::RateReceived,
1456 Action::CantDo,
1457 Action::Dispute,
1458 Action::AdminCancel,
1459 Action::AdminCanceled,
1460 Action::AdminSettle,
1461 Action::AdminSettled,
1462 Action::AdminAddSolver,
1463 Action::AdminTakeDispute,
1464 Action::AdminTookDispute,
1465 Action::PaymentFailed,
1466 Action::InvoiceUpdated,
1467 Action::SendDm,
1468 Action::TradePubkey,
1469 Action::RestoreSession,
1470 Action::LastTradeIndex,
1471 Action::Orders,
1472 Action::AddCashuEscrow,
1473 Action::CashuEscrowLocked,
1474 Action::CashuPmSignature,
1475 Action::AddBondInvoice,
1476 ];
1477 for action in unrelated {
1478 for payload in [
1479 Payload::PayerDeclaration(PayerDeclaration::new("a".repeat(64))),
1480 Payload::PaymentHistory(PaymentHistory::unavailable("a".repeat(64))),
1481 ] {
1482 let kind =
1483 MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1484 assert!(!kind.verify(), "{action:?} must reject {:?}", kind.payload);
1485 }
1486 }
1487 }
1488
1489 #[test]
1490 fn test_bond_payout_ack_actions_verify_and_wire_format() {
1491 use crate::message::BondResolution;
1492
1493 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1494
1495 let order = || SmallOrder {
1498 id: Some(order_id),
1499 kind: None,
1500 status: None,
1501 amount: 500,
1502 fiat_code: "USD".to_string(),
1503 min_amount: None,
1504 max_amount: None,
1505 fiat_amount: 0,
1506 payment_method: "lightning".to_string(),
1507 premium: 0,
1508 buyer_trade_pubkey: None,
1509 seller_trade_pubkey: None,
1510 buyer_invoice: None,
1511 created_at: None,
1512 expires_at: None,
1513 };
1514
1515 for (action, discriminator) in [
1521 (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1522 (Action::BondPayoutCompleted, "bond-payout-completed"),
1523 (Action::BondSlashed, "bond-slashed"),
1524 ] {
1525 let ok = Message::Order(MessageKind::new(
1527 Some(order_id),
1528 Some(1),
1529 Some(2),
1530 action.clone(),
1531 Some(Payload::Order(order())),
1532 ));
1533 assert!(ok.verify(), "{action:?} + Order should verify");
1534
1535 let no_id = Message::Order(MessageKind::new(
1537 None,
1538 Some(1),
1539 Some(2),
1540 action.clone(),
1541 Some(Payload::Order(order())),
1542 ));
1543 assert!(!no_id.verify(), "{action:?} without id must be rejected");
1544
1545 let with_resolution = Message::Order(MessageKind::new(
1547 Some(order_id),
1548 Some(1),
1549 Some(2),
1550 action.clone(),
1551 Some(Payload::BondResolution(BondResolution {
1552 slash_seller: true,
1553 slash_buyer: false,
1554 })),
1555 ));
1556 assert!(
1557 !with_resolution.verify(),
1558 "{action:?} + BondResolution must be rejected"
1559 );
1560
1561 let with_request = Message::Order(MessageKind::new(
1563 Some(order_id),
1564 Some(1),
1565 Some(2),
1566 action.clone(),
1567 Some(Payload::BondPayoutRequest(BondPayoutRequest {
1568 order: order(),
1569 slashed_at: 0,
1570 })),
1571 ));
1572 assert!(
1573 !with_request.verify(),
1574 "{action:?} + BondPayoutRequest must be rejected"
1575 );
1576
1577 let json = ok.as_json().unwrap();
1579 assert!(
1580 json.contains(&format!("\"action\":\"{discriminator}\"")),
1581 "expected kebab-case discriminator {discriminator}, got: {json}"
1582 );
1583 let decoded = Message::from_json(&json).unwrap();
1584 assert!(decoded.verify());
1585 assert_eq!(decoded.inner_action(), Some(action));
1586 }
1587 }
1588
1589 #[test]
1590 fn test_payment_failed_payload() {
1591 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1592
1593 let payment_failed_info = crate::message::PaymentFailedInfo {
1595 payment_attempts: 3,
1596 payment_retries_interval: 60,
1597 };
1598
1599 let payload = Payload::PaymentFailed(payment_failed_info);
1600 let message = Message::Order(MessageKind::new(
1601 Some(uuid),
1602 Some(1),
1603 Some(2),
1604 Action::PaymentFailed,
1605 Some(payload),
1606 ));
1607
1608 assert!(message.verify());
1610
1611 let message_json = message.as_json().unwrap();
1613
1614 let deserialized_message = Message::from_json(&message_json).unwrap();
1616 assert!(deserialized_message.verify());
1617
1618 if let Message::Order(kind) = deserialized_message {
1620 if let Some(Payload::PaymentFailed(info)) = kind.payload {
1621 assert_eq!(info.payment_attempts, 3);
1622 assert_eq!(info.payment_retries_interval, 60);
1623 } else {
1624 panic!("Expected PaymentFailed payload");
1625 }
1626 } else {
1627 panic!("Expected Order message");
1628 }
1629 }
1630
1631 #[test]
1632 fn test_message_payload_signature() {
1633 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1634 let peer = Peer::new(
1635 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1636 None, );
1638 let payload = Payload::Peer(peer);
1639 let test_message = Message::Order(MessageKind::new(
1640 Some(uuid),
1641 Some(1),
1642 Some(2),
1643 Action::FiatSentOk,
1644 Some(payload),
1645 ));
1646 assert!(test_message.verify());
1647 let test_message_json = test_message.as_json().unwrap();
1648 let trade_keys =
1650 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1651 .unwrap();
1652 let sig = Message::sign(test_message_json.clone(), &trade_keys);
1653
1654 assert!(Message::verify_signature(
1655 test_message_json,
1656 trade_keys.public_key(),
1657 sig
1658 ));
1659 }
1660
1661 #[test]
1662 fn test_restore_session_message() {
1663 let restore_request_message = Message::Restore(MessageKind::new(
1665 None,
1666 None,
1667 None,
1668 Action::RestoreSession,
1669 None,
1670 ));
1671
1672 assert!(restore_request_message.verify());
1674 assert_eq!(
1675 restore_request_message.inner_action(),
1676 Some(Action::RestoreSession)
1677 );
1678
1679 let message_json = restore_request_message.as_json().unwrap();
1681 let deserialized_message = Message::from_json(&message_json).unwrap();
1682 assert!(deserialized_message.verify());
1683 assert_eq!(
1684 deserialized_message.inner_action(),
1685 Some(Action::RestoreSession)
1686 );
1687
1688 let restored_orders = vec![
1690 crate::message::RestoredOrdersInfo {
1691 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1692 trade_index: 1,
1693 status: "active".to_string(),
1694 counterparty_trade_pubkey: Some(
1695 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1696 ),
1697 },
1698 crate::message::RestoredOrdersInfo {
1699 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1700 trade_index: 2,
1701 status: "success".to_string(),
1702 counterparty_trade_pubkey: None,
1703 },
1704 ];
1705
1706 let restored_disputes = vec![
1707 crate::message::RestoredDisputesInfo {
1708 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1709 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1710 trade_index: 1,
1711 status: "initiated".to_string(),
1712 initiator: Some(crate::message::DisputeInitiator::Buyer),
1713 solver_pubkey: None,
1714 },
1715 crate::message::RestoredDisputesInfo {
1716 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1717 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1718 trade_index: 2,
1719 status: "in-progress".to_string(),
1720 initiator: None,
1721 solver_pubkey: Some(
1722 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1723 ),
1724 },
1725 crate::message::RestoredDisputesInfo {
1726 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1727 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1728 trade_index: 3,
1729 status: "initiated".to_string(),
1730 initiator: Some(crate::message::DisputeInitiator::Seller),
1731 solver_pubkey: None,
1732 },
1733 ];
1734
1735 let restore_session_info = crate::message::RestoreSessionInfo {
1736 restore_orders: restored_orders.clone(),
1737 restore_disputes: restored_disputes.clone(),
1738 };
1739
1740 let restore_data_payload = Payload::RestoreData(restore_session_info);
1741 let restore_data_message = Message::Restore(MessageKind::new(
1742 None,
1743 None,
1744 None,
1745 Action::RestoreSession,
1746 Some(restore_data_payload),
1747 ));
1748
1749 assert!(!restore_data_message.verify());
1751
1752 let message_json = restore_data_message.as_json().unwrap();
1754 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1755
1756 if let Message::Restore(kind) = deserialized_restore_message {
1757 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1758 assert_eq!(session_info.restore_disputes.len(), 3);
1759 assert_eq!(
1760 session_info.restore_disputes[0].initiator,
1761 Some(crate::message::DisputeInitiator::Buyer)
1762 );
1763 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1764 assert_eq!(session_info.restore_disputes[1].initiator, None);
1765 assert_eq!(
1766 session_info.restore_disputes[1].solver_pubkey,
1767 Some(
1768 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1769 .to_string()
1770 )
1771 );
1772 assert_eq!(
1773 session_info.restore_disputes[2].initiator,
1774 Some(crate::message::DisputeInitiator::Seller)
1775 );
1776 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1777 assert_eq!(
1778 session_info.restore_orders[0].counterparty_trade_pubkey,
1779 Some(
1780 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1781 .to_string()
1782 )
1783 );
1784 assert!(session_info.restore_orders[1]
1785 .counterparty_trade_pubkey
1786 .is_none());
1787 } else {
1788 panic!("Expected RestoreData payload");
1789 }
1790 } else {
1791 panic!("Expected Restore message");
1792 }
1793 }
1794
1795 #[test]
1798 fn restored_order_without_counterparty_pubkey_still_parses() {
1799 let json = r#"{
1800 "order_id": "308e1272-d5f4-47e6-bd97-3504baea9c23",
1801 "trade_index": 1,
1802 "status": "active"
1803 }"#;
1804
1805 let order: crate::message::RestoredOrdersInfo = serde_json::from_str(json).unwrap();
1806
1807 assert_eq!(order.trade_index, 1);
1808 assert_eq!(order.status, "active");
1809 assert!(order.counterparty_trade_pubkey.is_none());
1810 }
1811
1812 #[test]
1813 fn test_restore_session_message_validation() {
1814 let restore_request_message = Message::Restore(MessageKind::new(
1816 None,
1817 None,
1818 None,
1819 Action::RestoreSession,
1820 None, ));
1822
1823 assert!(restore_request_message.verify());
1825
1826 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1828 let wrong_message = Message::Restore(MessageKind::new(
1829 None,
1830 None,
1831 None,
1832 Action::RestoreSession,
1833 Some(wrong_payload),
1834 ));
1835
1836 assert!(!wrong_message.verify());
1838
1839 let with_id = Message::Restore(MessageKind::new(
1841 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1842 None,
1843 None,
1844 Action::RestoreSession,
1845 None,
1846 ));
1847 assert!(with_id.verify());
1848
1849 let with_request_id = Message::Restore(MessageKind::new(
1850 None,
1851 Some(42),
1852 None,
1853 Action::RestoreSession,
1854 None,
1855 ));
1856 assert!(with_request_id.verify());
1857
1858 let with_trade_index = Message::Restore(MessageKind::new(
1859 None,
1860 None,
1861 Some(7),
1862 Action::RestoreSession,
1863 None,
1864 ));
1865 assert!(with_trade_index.verify());
1866 }
1867
1868 #[test]
1869 fn test_restore_session_message_constructor() {
1870 let restore_request_message = Message::new_restore(None);
1872
1873 assert!(matches!(restore_request_message, Message::Restore(_)));
1874 assert!(restore_request_message.verify());
1875 assert_eq!(
1876 restore_request_message.inner_action(),
1877 Some(Action::RestoreSession)
1878 );
1879
1880 let restore_session_info = crate::message::RestoreSessionInfo {
1882 restore_orders: vec![],
1883 restore_disputes: vec![],
1884 };
1885 let restore_data_message =
1886 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1887
1888 assert!(matches!(restore_data_message, Message::Restore(_)));
1889 assert!(!restore_data_message.verify());
1890 }
1891
1892 #[test]
1893 fn test_last_trade_index_valid_message() {
1894 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1895 let msg = Message::Restore(kind);
1896
1897 assert!(msg.verify());
1898
1899 let json = msg.as_json().unwrap();
1901 let decoded = Message::from_json(&json).unwrap();
1902 assert!(decoded.verify());
1903
1904 let inner = decoded.get_inner_message_kind();
1906 assert_eq!(inner.trade_index(), 7);
1907 assert_eq!(inner.has_trade_index(), (true, 7));
1908 }
1909
1910 #[test]
1911 fn test_last_trade_index_without_id_is_valid() {
1912 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1914 let msg = Message::Restore(kind);
1915 assert!(msg.verify());
1916 }
1917
1918 #[test]
1919 fn test_last_trade_index_with_payload_fails_validation() {
1920 let kind = MessageKind::new(
1922 None,
1923 None,
1924 Some(3),
1925 Action::LastTradeIndex,
1926 Some(Payload::TextMessage("ignored".to_string())),
1927 );
1928 let msg = Message::Restore(kind);
1929 assert!(!msg.verify());
1930 }
1931
1932 #[test]
1933 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1934 use crate::message::BondResolution;
1935
1936 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1937
1938 for action in [Action::AdminSettle, Action::AdminCancel] {
1939 let with_resolution = Message::Order(MessageKind::new(
1940 Some(uuid),
1941 Some(1),
1942 Some(2),
1943 action.clone(),
1944 Some(Payload::BondResolution(BondResolution {
1945 slash_seller: true,
1946 slash_buyer: false,
1947 })),
1948 ));
1949 assert!(
1950 with_resolution.verify(),
1951 "{action:?} + BondResolution should verify"
1952 );
1953
1954 let without_payload = Message::Order(MessageKind::new(
1955 Some(uuid),
1956 Some(1),
1957 Some(2),
1958 action.clone(),
1959 None,
1960 ));
1961 assert!(without_payload.verify(), "{action:?} + None should verify");
1962
1963 let wrong = Message::Order(MessageKind::new(
1965 Some(uuid),
1966 Some(1),
1967 Some(2),
1968 action.clone(),
1969 Some(Payload::TextMessage("nope".to_string())),
1970 ));
1971 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1972
1973 let no_id = Message::Order(MessageKind::new(
1975 None,
1976 Some(1),
1977 Some(2),
1978 action,
1979 Some(Payload::BondResolution(BondResolution {
1980 slash_seller: false,
1981 slash_buyer: false,
1982 })),
1983 ));
1984 assert!(!no_id.verify(), "admin action without id must be rejected");
1985 }
1986 }
1987
1988 #[test]
1989 fn test_bond_resolution_rejected_on_non_admin_actions() {
1990 use crate::message::BondResolution;
1991
1992 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1993 let payload = Payload::BondResolution(BondResolution {
1994 slash_seller: true,
1995 slash_buyer: true,
1996 });
1997
1998 for action in [
2002 Action::NewOrder,
2003 Action::TakeSell,
2004 Action::TakeBuy,
2005 Action::PayInvoice,
2006 Action::PayBondInvoice,
2007 Action::FiatSent,
2008 Action::FiatSentOk,
2009 Action::Release,
2010 Action::Released,
2011 Action::Cancel,
2012 Action::Canceled,
2013 Action::CooperativeCancelInitiatedByYou,
2014 Action::CooperativeCancelInitiatedByPeer,
2015 Action::DisputeInitiatedByYou,
2016 Action::DisputeInitiatedByPeer,
2017 Action::CooperativeCancelAccepted,
2018 Action::BuyerInvoiceAccepted,
2019 Action::BondInvoiceAccepted,
2020 Action::PurchaseCompleted,
2021 Action::BondPayoutCompleted,
2022 Action::BondSlashed,
2023 Action::HoldInvoicePaymentAccepted,
2024 Action::HoldInvoicePaymentSettled,
2025 Action::HoldInvoicePaymentCanceled,
2026 Action::WaitingSellerToPay,
2027 Action::WaitingBuyerInvoice,
2028 Action::AddInvoice,
2029 Action::AddBondInvoice,
2030 Action::BuyerTookOrder,
2031 Action::Rate,
2032 Action::RateUser,
2033 Action::RateReceived,
2034 Action::CantDo,
2035 Action::Dispute,
2036 Action::AdminCanceled,
2037 Action::AdminSettled,
2038 Action::AdminAddSolver,
2039 Action::AdminTakeDispute,
2040 Action::AdminTookDispute,
2041 Action::PaymentFailed,
2042 Action::InvoiceUpdated,
2043 Action::SendDm,
2044 Action::TradePubkey,
2045 Action::RestoreSession,
2046 Action::LastTradeIndex,
2047 Action::Orders,
2048 ] {
2049 let msg = Message::Order(MessageKind::new(
2050 Some(uuid),
2051 Some(1),
2052 Some(2),
2053 action.clone(),
2054 Some(payload.clone()),
2055 ));
2056 assert!(
2057 !msg.verify(),
2058 "{action:?} must reject BondResolution payload"
2059 );
2060 }
2061 }
2062
2063 #[test]
2064 fn test_bond_resolution_wire_format() {
2065 use crate::message::BondResolution;
2066
2067 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2068 let msg = Message::Order(MessageKind::new(
2069 Some(uuid),
2070 None,
2071 None,
2072 Action::AdminCancel,
2073 Some(Payload::BondResolution(BondResolution {
2074 slash_seller: true,
2075 slash_buyer: false,
2076 })),
2077 ));
2078
2079 let json = msg.as_json().unwrap();
2080 assert!(
2082 json.contains("\"bond_resolution\""),
2083 "expected snake_case discriminator, got: {json}"
2084 );
2085 assert!(json.contains("\"slash_seller\":true"));
2086 assert!(json.contains("\"slash_buyer\":false"));
2087
2088 let decoded = Message::from_json(&json).unwrap();
2090 assert!(decoded.verify());
2091 if let Message::Order(kind) = decoded {
2092 match kind.payload {
2093 Some(Payload::BondResolution(b)) => {
2094 assert!(b.slash_seller);
2095 assert!(!b.slash_buyer);
2096 }
2097 other => panic!("expected BondResolution payload, got {other:?}"),
2098 }
2099 } else {
2100 panic!("expected Order message");
2101 }
2102 }
2103
2104 #[test]
2105 fn test_bond_resolution_legacy_null_payload() {
2106 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2110 let json = format!(
2111 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
2112 );
2113 let msg = Message::from_json(&json).unwrap();
2114 assert!(msg.verify());
2115 }
2116
2117 #[test]
2118 fn test_pay_bond_invoice_wire_format_and_verify() {
2119 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2120 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
2121
2122 let msg = Message::Order(MessageKind::new(
2123 Some(uuid),
2124 Some(1),
2125 Some(2),
2126 Action::PayBondInvoice,
2127 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2128 ));
2129 assert!(msg.verify());
2130
2131 let json = msg.as_json().unwrap();
2133 assert!(
2134 json.contains("\"action\":\"pay-bond-invoice\""),
2135 "expected kebab-case discriminator, got: {json}"
2136 );
2137
2138 let decoded = Message::from_json(&json).unwrap();
2140 assert!(decoded.verify());
2141 assert!(matches!(
2142 decoded.inner_action(),
2143 Some(Action::PayBondInvoice)
2144 ));
2145
2146 let no_id = Message::Order(MessageKind::new(
2148 None,
2149 Some(1),
2150 Some(2),
2151 Action::PayBondInvoice,
2152 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2153 ));
2154 assert!(!no_id.verify());
2155
2156 let wrong_payload = Message::Order(MessageKind::new(
2158 Some(uuid),
2159 Some(1),
2160 Some(2),
2161 Action::PayBondInvoice,
2162 Some(Payload::TextMessage("nope".to_string())),
2163 ));
2164 assert!(!wrong_payload.verify());
2165
2166 let no_payload = Message::Order(MessageKind::new(
2168 Some(uuid),
2169 Some(1),
2170 Some(2),
2171 Action::PayBondInvoice,
2172 None,
2173 ));
2174 assert!(!no_payload.verify());
2175 }
2176
2177 #[test]
2178 fn test_add_bond_invoice_wire_format_and_verify() {
2179 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2180 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
2181
2182 let msg = Message::Order(MessageKind::new(
2183 Some(uuid),
2184 Some(1),
2185 Some(2),
2186 Action::AddBondInvoice,
2187 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2188 ));
2189 assert!(msg.verify());
2190
2191 let json = msg.as_json().unwrap();
2193 assert!(
2194 json.contains("\"action\":\"add-bond-invoice\""),
2195 "expected kebab-case discriminator, got: {json}"
2196 );
2197
2198 let decoded = Message::from_json(&json).unwrap();
2200 assert!(decoded.verify());
2201 assert!(matches!(
2202 decoded.inner_action(),
2203 Some(Action::AddBondInvoice)
2204 ));
2205
2206 let no_id = Message::Order(MessageKind::new(
2208 None,
2209 Some(1),
2210 Some(2),
2211 Action::AddBondInvoice,
2212 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2213 ));
2214 assert!(!no_id.verify());
2215
2216 let wrong_payload = Message::Order(MessageKind::new(
2218 Some(uuid),
2219 Some(1),
2220 Some(2),
2221 Action::AddBondInvoice,
2222 Some(Payload::TextMessage("nope".to_string())),
2223 ));
2224 assert!(!wrong_payload.verify());
2225
2226 let no_payload = Message::Order(MessageKind::new(
2228 Some(uuid),
2229 Some(1),
2230 Some(2),
2231 Action::AddBondInvoice,
2232 None,
2233 ));
2234 assert!(!no_payload.verify());
2235
2236 if let Message::Order(kind) = &msg {
2238 assert_eq!(kind.get_payment_request(), Some(bolt11));
2239 } else {
2240 panic!("expected Message::Order");
2241 }
2242 }
2243
2244 #[test]
2245 fn test_restored_dispute_helper_serialization_roundtrip() {
2246 use crate::message::RestoredDisputeHelper;
2247
2248 let helper = RestoredDisputeHelper {
2249 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
2250 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
2251 dispute_status: "initiated".to_string(),
2252 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
2253 master_seller_pubkey: Some("npub1sellerkey".to_string()),
2254 trade_index_buyer: Some(1),
2255 trade_index_seller: Some(2),
2256 buyer_dispute: true,
2257 seller_dispute: false,
2258 solver_pubkey: None,
2259 };
2260
2261 let json = serde_json::to_string(&helper).unwrap();
2262 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
2263
2264 assert_eq!(deserialized.dispute_id, helper.dispute_id);
2265 assert_eq!(deserialized.order_id, helper.order_id);
2266 assert_eq!(deserialized.dispute_status, helper.dispute_status);
2267 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
2268 assert_eq!(
2269 deserialized.master_seller_pubkey,
2270 helper.master_seller_pubkey
2271 );
2272 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
2273 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
2274 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
2275 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
2276 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
2277
2278 let helper_seller_dispute = RestoredDisputeHelper {
2279 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
2280 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
2281 dispute_status: "in-progress".to_string(),
2282 master_buyer_pubkey: None,
2283 master_seller_pubkey: None,
2284 trade_index_buyer: None,
2285 trade_index_seller: None,
2286 buyer_dispute: false,
2287 seller_dispute: true,
2288 solver_pubkey: Some(
2289 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
2290 ),
2291 };
2292
2293 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
2294 let deserialized_seller: RestoredDisputeHelper =
2295 serde_json::from_str(&json_seller).unwrap();
2296
2297 assert_eq!(
2298 deserialized_seller.dispute_id,
2299 helper_seller_dispute.dispute_id
2300 );
2301 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
2302 assert_eq!(
2303 deserialized_seller.dispute_status,
2304 helper_seller_dispute.dispute_status
2305 );
2306 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
2307 assert_eq!(deserialized_seller.master_seller_pubkey, None);
2308 assert_eq!(deserialized_seller.trade_index_buyer, None);
2309 assert_eq!(deserialized_seller.trade_index_seller, None);
2310 assert!(!deserialized_seller.buyer_dispute);
2311 assert!(deserialized_seller.seller_dispute);
2312 assert_eq!(
2313 deserialized_seller.solver_pubkey,
2314 helper_seller_dispute.solver_pubkey
2315 );
2316 }
2317
2318 fn sample_lock_proof() -> CashuLockProof {
2319 CashuLockProof::new(
2320 "cashuAeyJ0b2tlbiI6dGVzdA".to_string(),
2321 "https://mint.example".to_string(),
2322 "02b_buyer".to_string(),
2323 "02s_seller".to_string(),
2324 "02m_mostro".to_string(),
2325 )
2326 }
2327
2328 #[test]
2329 fn test_cashu_lock_proof_json_round_trip() {
2330 let proof = sample_lock_proof();
2331 let json = proof.as_json().unwrap();
2332 let back = CashuLockProof::from_json(&json).unwrap();
2333 assert_eq!(back, proof);
2334 }
2335
2336 #[test]
2337 fn test_cashu_lock_proof_fee_token_round_trip() {
2338 let proof = sample_lock_proof().with_fee_token("cashuAfee".to_string());
2339 let json = proof.as_json().unwrap();
2340 assert!(json.contains("fee_token"));
2341 let back = CashuLockProof::from_json(&json).unwrap();
2342 assert_eq!(back, proof);
2343 assert_eq!(back.fee_token.as_deref(), Some("cashuAfee"));
2344 }
2345
2346 #[test]
2347 fn test_cashu_lock_proof_without_fee_token_is_omitted_and_defaults_to_none() {
2348 let proof = sample_lock_proof();
2351 assert_eq!(proof.fee_token, None);
2352 let json = proof.as_json().unwrap();
2353 assert!(!json.contains("fee_token"));
2354 let back = CashuLockProof::from_json(&json).unwrap();
2355 assert_eq!(back.fee_token, None);
2356 }
2357
2358 #[test]
2359 fn test_add_cashu_escrow_verifies_with_lock_proof() {
2360 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2361 let payload = Payload::CashuLockProof(sample_lock_proof());
2362 let kind = MessageKind::new(
2363 Some(order_id),
2364 None,
2365 None,
2366 Action::AddCashuEscrow,
2367 Some(payload),
2368 );
2369 assert!(
2370 kind.verify(),
2371 "CashuLockProof must verify on AddCashuEscrow"
2372 );
2373
2374 let json = Message::Order(kind).as_json().unwrap();
2377 assert!(json.contains("cashu_lock_proof"));
2378 assert!(Message::from_json(&json).unwrap().verify());
2379 }
2380
2381 #[test]
2382 fn test_add_cashu_escrow_requires_id_and_right_payload() {
2383 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2384
2385 let no_id = MessageKind::new(
2387 None,
2388 None,
2389 None,
2390 Action::AddCashuEscrow,
2391 Some(Payload::CashuLockProof(sample_lock_proof())),
2392 );
2393 assert!(
2394 !no_id.verify(),
2395 "AddCashuEscrow without id must be rejected"
2396 );
2397
2398 let wrong_payload = MessageKind::new(
2400 Some(order_id),
2401 None,
2402 None,
2403 Action::AddCashuEscrow,
2404 Some(Payload::TextMessage("not a lock proof".to_string())),
2405 );
2406 assert!(
2407 !wrong_payload.verify(),
2408 "AddCashuEscrow with non-lock-proof payload must be rejected"
2409 );
2410 }
2411
2412 #[test]
2413 fn test_cashu_pm_signature_verifies_with_signatures() {
2414 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2415 let kind = MessageKind::new(
2416 Some(order_id),
2417 None,
2418 None,
2419 Action::CashuPmSignature,
2420 Some(Payload::CashuSignatures(vec![
2421 CashuProofSignature::new("secret-0".to_string(), "deadbeef".to_string()),
2422 CashuProofSignature::new("secret-1".to_string(), "c0ffee".to_string()),
2423 ])),
2424 );
2425 assert!(
2426 kind.verify(),
2427 "CashuSignatures must verify on CashuPmSignature"
2428 );
2429
2430 let json = Message::Order(kind).as_json().unwrap();
2431 assert!(json.contains("cashu_signatures"));
2432 assert!(Message::from_json(&json).unwrap().verify());
2433
2434 let wrong = MessageKind::new(Some(order_id), None, None, Action::CashuPmSignature, None);
2436 assert!(
2437 !wrong.verify(),
2438 "CashuPmSignature without a signature payload must be rejected"
2439 );
2440
2441 let empty = MessageKind::new(
2444 Some(order_id),
2445 None,
2446 None,
2447 Action::CashuPmSignature,
2448 Some(Payload::CashuSignatures(vec![])),
2449 );
2450 assert!(
2451 !empty.verify(),
2452 "CashuPmSignature with an empty signature set must be rejected"
2453 );
2454 }
2455
2456 #[test]
2457 fn test_cashu_escrow_locked_is_informational() {
2458 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2459
2460 let ok = MessageKind::new(Some(order_id), None, None, Action::CashuEscrowLocked, None);
2462 assert!(ok.verify(), "CashuEscrowLocked with id must verify");
2463
2464 let no_id = MessageKind::new(None, None, None, Action::CashuEscrowLocked, None);
2466 assert!(
2467 !no_id.verify(),
2468 "CashuEscrowLocked without id must be rejected"
2469 );
2470 }
2471
2472 fn sample_declaration() -> PayerDeclaration {
2473 PayerDeclaration::new(payment_hash("EU|SEPA|DE89370400440532013000|ALICE SMITH"))
2474 }
2475
2476 fn sample_history() -> PaymentHistory {
2477 PaymentHistory {
2478 payment_hash: sample_declaration().payment_hash,
2479 buyer_mode: BuyerMode::Reputation,
2480 successful_trades: 47,
2481 distinct_counterparties: 29,
2482 experienced_counterparties: 11,
2483 first_success_at: Some(1_762_128_000),
2484 last_success_at: Some(1_787_654_321),
2485 }
2486 }
2487
2488 #[test]
2489 fn test_payer_history_actions_use_kebab_case_wire_names() {
2490 for (action, wire) in [
2491 (Action::DeclarePayer, "\"declare-payer\""),
2492 (Action::PayerDeclared, "\"payer-declared\""),
2493 (Action::PaymentHistory, "\"payment-history\""),
2494 ] {
2495 assert_eq!(serde_json::to_string(&action).unwrap(), wire);
2496 assert_eq!(serde_json::from_str::<Action>(wire).unwrap(), action);
2497 }
2498 }
2499
2500 #[test]
2501 fn test_declare_payer_requires_id_and_declaration() {
2502 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2503 let decl = Some(Payload::PayerDeclaration(sample_declaration()));
2504
2505 let ok = MessageKind::new(
2506 Some(order_id),
2507 Some(1),
2508 Some(7),
2509 Action::DeclarePayer,
2510 decl.clone(),
2511 );
2512 assert!(
2513 ok.verify(),
2514 "DeclarePayer with id and declaration must verify"
2515 );
2516
2517 let no_id = MessageKind::new(None, None, None, Action::DeclarePayer, decl);
2518 assert!(!no_id.verify(), "DeclarePayer without id must be rejected");
2519
2520 let no_payload = MessageKind::new(Some(order_id), None, None, Action::DeclarePayer, None);
2521 assert!(
2522 !no_payload.verify(),
2523 "DeclarePayer without payload must be rejected"
2524 );
2525
2526 let wrong = MessageKind::new(
2527 Some(order_id),
2528 None,
2529 None,
2530 Action::DeclarePayer,
2531 Some(Payload::TextMessage(
2532 "plaintext must never be sent".to_string(),
2533 )),
2534 );
2535 assert!(
2536 !wrong.verify(),
2537 "DeclarePayer with a non-declaration payload must be rejected"
2538 );
2539 }
2540
2541 #[test]
2542 fn test_payer_declared_requires_id_and_declaration() {
2543 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2544 let decl = Some(Payload::PayerDeclaration(sample_declaration()));
2545
2546 let ok = MessageKind::new(
2547 Some(order_id),
2548 None,
2549 None,
2550 Action::PayerDeclared,
2551 decl.clone(),
2552 );
2553 assert!(
2554 ok.verify(),
2555 "PayerDeclared with id and declaration must verify"
2556 );
2557
2558 let no_id = MessageKind::new(None, None, None, Action::PayerDeclared, decl);
2559 assert!(!no_id.verify(), "PayerDeclared without id must be rejected");
2560
2561 let no_payload = MessageKind::new(Some(order_id), None, None, Action::PayerDeclared, None);
2562 assert!(
2563 !no_payload.verify(),
2564 "PayerDeclared without payload must be rejected"
2565 );
2566
2567 let wrong = MessageKind::new(
2568 Some(order_id),
2569 None,
2570 None,
2571 Action::PayerDeclared,
2572 Some(Payload::PaymentHistory(sample_history())),
2573 );
2574 assert!(
2575 !wrong.verify(),
2576 "PayerDeclared with a history payload must be rejected"
2577 );
2578 }
2579
2580 #[test]
2581 fn test_payment_history_accepts_query_and_reply_shapes() {
2582 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2583
2584 let query = MessageKind::new(Some(order_id), Some(3), None, Action::PaymentHistory, None);
2586 assert!(
2587 query.verify(),
2588 "PaymentHistory query (no payload) must verify"
2589 );
2590
2591 let reply = MessageKind::new(
2593 Some(order_id),
2594 None,
2595 None,
2596 Action::PaymentHistory,
2597 Some(Payload::PaymentHistory(sample_history())),
2598 );
2599 assert!(reply.verify(), "PaymentHistory reply must verify");
2600
2601 let no_id = MessageKind::new(None, None, None, Action::PaymentHistory, None);
2602 assert!(
2603 !no_id.verify(),
2604 "PaymentHistory without id must be rejected"
2605 );
2606
2607 let wrong = MessageKind::new(
2608 Some(order_id),
2609 None,
2610 None,
2611 Action::PaymentHistory,
2612 Some(Payload::PayerDeclaration(sample_declaration())),
2613 );
2614 assert!(
2615 !wrong.verify(),
2616 "PaymentHistory with a declaration payload must be rejected"
2617 );
2618 }
2619
2620 #[test]
2621 fn test_payer_payloads_round_trip_with_snake_case_keys() {
2622 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2623
2624 let declare = Message::Order(MessageKind::new(
2625 Some(order_id),
2626 Some(981_231),
2627 Some(7),
2628 Action::DeclarePayer,
2629 Some(Payload::PayerDeclaration(sample_declaration())),
2630 ));
2631 let json = declare.as_json().unwrap();
2632 assert!(json.contains("\"action\":\"declare-payer\""));
2633 assert!(json.contains("\"payer_declaration\":{\"payment_hash\":"));
2634 assert!(Message::from_json(&json).unwrap().verify());
2635
2636 let push = Message::Order(MessageKind::new(
2637 Some(order_id),
2638 None,
2639 None,
2640 Action::PaymentHistory,
2641 Some(Payload::PaymentHistory(sample_history())),
2642 ));
2643 let json = push.as_json().unwrap();
2644 assert!(json.contains("\"payment_history\":{"));
2645 assert!(json.contains("\"buyer_mode\":\"reputation\""));
2646 assert!(json.contains("\"experienced_counterparties\":11"));
2647 let back = Message::from_json(&json).unwrap();
2648 assert!(back.verify());
2649 match back.get_inner_message_kind().payload.clone() {
2650 Some(Payload::PaymentHistory(h)) => assert_eq!(h, sample_history()),
2651 other => panic!("expected a PaymentHistory payload, got {other:?}"),
2652 }
2653 }
2654
2655 #[test]
2656 fn test_payment_history_parses_the_protocol_wire_example() {
2657 let hash = sample_declaration().payment_hash;
2660 let json = format!(
2661 r#"{{"payment_history": {{"payment_hash": "{hash}", "buyer_mode": "full_privacy",
2662 "successful_trades": 0, "distinct_counterparties": 0,
2663 "experienced_counterparties": 0,
2664 "first_success_at": null, "last_success_at": null}}}}"#
2665 );
2666 let payload: Payload = serde_json::from_str(&json).unwrap();
2667 match payload {
2668 Payload::PaymentHistory(h) => assert_eq!(h, PaymentHistory::unavailable(hash)),
2669 other => panic!("expected a PaymentHistory payload, got {other:?}"),
2670 }
2671 }
2672
2673 #[test]
2676 fn reputation_messages_have_the_documented_wire_shape() {
2677 let export = Message::new_order(
2678 None,
2679 Some(4126),
2680 None,
2681 Action::ExportReputation,
2682 Some(Payload::ReputationExportRequest(
2683 super::ReputationExportRequest {
2684 destination: "ab".repeat(32),
2685 rebind: None,
2686 },
2687 )),
2688 );
2689 let json: serde_json::Value = serde_json::from_str(&export.as_json().unwrap()).unwrap();
2690 assert_eq!(
2691 json,
2692 serde_json::json!({ "order": {
2693 "version": 2,
2694 "request_id": 4126,
2695 "trade_index": null,
2696 "action": "export-reputation",
2697 "payload": { "reputation_export_request": {
2698 "destination": "ab".repeat(32),
2699 "rebind": null,
2700 }},
2701 }})
2702 );
2703
2704 for (action, wire) in [
2705 (Action::ReputationExported, "reputation-exported"),
2706 (Action::ImportReputation, "import-reputation"),
2707 ] {
2708 let message = Message::new_order(
2709 None,
2710 None,
2711 None,
2712 action,
2713 Some(Payload::ReputationAttestation("{}".into())),
2714 );
2715 let json: serde_json::Value =
2716 serde_json::from_str(&message.as_json().unwrap()).unwrap();
2717 assert_eq!(json["order"]["action"], wire);
2718 assert_eq!(json["order"]["payload"]["reputation_attestation"], "{}");
2719 }
2720
2721 let imported = Message::new_order(None, None, None, Action::ReputationImported, None);
2722 let json: serde_json::Value = serde_json::from_str(&imported.as_json().unwrap()).unwrap();
2723 assert_eq!(json["order"]["action"], "reputation-imported");
2724 assert!(json["order"]["payload"].is_null());
2725 }
2726
2727 #[test]
2728 fn an_export_request_without_rebind_deserialises() {
2729 let json = r#"{"order":{"version":2,"action":"export-reputation","payload":{"reputation_export_request":{"destination":"ab"}}}}"#;
2730 let message = Message::from_json(json).unwrap();
2731 let kind = message.get_inner_message_kind();
2732 match &kind.payload {
2733 Some(Payload::ReputationExportRequest(request)) => assert_eq!(request.rebind, None),
2734 other => panic!("unexpected payload {other:?}"),
2735 }
2736 assert!(kind.verify());
2737 }
2738
2739 #[test]
2740 fn reputation_actions_verify_only_without_an_id_and_with_their_payload() {
2741 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2742 let request = Payload::ReputationExportRequest(super::ReputationExportRequest {
2743 destination: "ab".repeat(32),
2744 rebind: None,
2745 });
2746 let attestation = Payload::ReputationAttestation("{}".into());
2747 let cases = [
2748 (Action::ExportReputation, Some(request.clone()), true),
2749 (Action::ExportReputation, Some(attestation.clone()), false),
2750 (Action::ExportReputation, None, false),
2751 (Action::ReputationExported, Some(attestation.clone()), true),
2752 (Action::ReputationExported, None, false),
2753 (Action::ImportReputation, Some(attestation.clone()), true),
2754 (Action::ImportReputation, Some(request.clone()), false),
2755 (Action::ReputationImported, None, true),
2756 (Action::ReputationImported, Some(attestation.clone()), false),
2757 ];
2758 for (action, payload, ok) in cases {
2759 let without_id = MessageKind::new(None, None, None, action.clone(), payload.clone());
2760 assert_eq!(without_id.verify(), ok, "{action:?} {payload:?}");
2761 let with_id = MessageKind::new(Some(order_id), None, None, action.clone(), payload);
2762 assert!(!with_id.verify(), "{action:?} must not carry an id");
2763 }
2764 }
2765
2766 #[test]
2773 fn reputation_messages_keep_protocol_version_2() {
2774 for action in [
2775 Action::ExportReputation,
2776 Action::ReputationExported,
2777 Action::ImportReputation,
2778 Action::ReputationImported,
2779 ] {
2780 assert_eq!(MessageKind::new(None, None, None, action, None).version, 2);
2781 }
2782 }
2783}