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 cashu_mint_url: None,
1233 };
1234 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1235 order,
1236 slashed_at: 1_734_000_000,
1237 });
1238 let kind = MessageKind::new(
1239 Some(order_id),
1240 None,
1241 None,
1242 Action::AddBondInvoice,
1243 Some(payload),
1244 );
1245 assert!(
1246 kind.verify(),
1247 "BondPayoutRequest must verify on AddBondInvoice"
1248 );
1249
1250 let m = Message::Order(kind);
1253 let json = m.as_json().unwrap();
1254 assert!(json.contains("bond_payout_request"));
1255 let back = Message::from_json(&json).unwrap();
1256 assert!(back.verify());
1257 }
1258
1259 #[test]
1260 fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1261 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1264 let order = SmallOrder {
1265 id: Some(order_id),
1266 kind: None,
1267 status: None,
1268 amount: 500,
1269 fiat_code: "USD".to_string(),
1270 min_amount: None,
1271 max_amount: None,
1272 fiat_amount: 0,
1273 payment_method: "lightning".to_string(),
1274 premium: 0,
1275 buyer_trade_pubkey: None,
1276 seller_trade_pubkey: None,
1277 buyer_invoice: None,
1278 created_at: None,
1279 expires_at: None,
1280 cashu_mint_url: None,
1281 };
1282
1283 let _exhaustive: fn(Action) = |a| match a {
1288 Action::AddBondInvoice => {}
1289 Action::NewOrder
1290 | Action::TakeSell
1291 | Action::TakeBuy
1292 | Action::PayInvoice
1293 | Action::PayBondInvoice
1294 | Action::FiatSent
1295 | Action::FiatSentOk
1296 | Action::Release
1297 | Action::Released
1298 | Action::Cancel
1299 | Action::Canceled
1300 | Action::CooperativeCancelInitiatedByYou
1301 | Action::CooperativeCancelInitiatedByPeer
1302 | Action::DisputeInitiatedByYou
1303 | Action::DisputeInitiatedByPeer
1304 | Action::CooperativeCancelAccepted
1305 | Action::BuyerInvoiceAccepted
1306 | Action::BondInvoiceAccepted
1307 | Action::PurchaseCompleted
1308 | Action::BondPayoutCompleted
1309 | Action::BondSlashed
1310 | Action::HoldInvoicePaymentAccepted
1311 | Action::HoldInvoicePaymentSettled
1312 | Action::HoldInvoicePaymentCanceled
1313 | Action::WaitingSellerToPay
1314 | Action::WaitingBuyerInvoice
1315 | Action::AddInvoice
1316 | Action::BuyerTookOrder
1317 | Action::Rate
1318 | Action::RateUser
1319 | Action::RateReceived
1320 | Action::CantDo
1321 | Action::Dispute
1322 | Action::AdminCancel
1323 | Action::AdminCanceled
1324 | Action::AdminSettle
1325 | Action::AdminSettled
1326 | Action::AdminAddSolver
1327 | Action::AdminTakeDispute
1328 | Action::AdminTookDispute
1329 | Action::PaymentFailed
1330 | Action::InvoiceUpdated
1331 | Action::SendDm
1332 | Action::TradePubkey
1333 | Action::RestoreSession
1334 | Action::LastTradeIndex
1335 | Action::AddCashuEscrow
1336 | Action::CashuEscrowLocked
1337 | Action::CashuPmSignature
1338 | Action::DeclarePayer
1339 | Action::PayerDeclared
1340 | Action::PaymentHistory
1341 | Action::ExportReputation
1342 | Action::ReputationExported
1343 | Action::ImportReputation
1344 | Action::ReputationImported
1345 | Action::Orders => {}
1346 };
1347
1348 let other_actions: &[Action] = &[
1349 Action::NewOrder,
1350 Action::TakeSell,
1351 Action::TakeBuy,
1352 Action::PayInvoice,
1353 Action::PayBondInvoice,
1354 Action::FiatSent,
1355 Action::FiatSentOk,
1356 Action::Release,
1357 Action::Released,
1358 Action::Cancel,
1359 Action::Canceled,
1360 Action::CooperativeCancelInitiatedByYou,
1361 Action::CooperativeCancelInitiatedByPeer,
1362 Action::DisputeInitiatedByYou,
1363 Action::DisputeInitiatedByPeer,
1364 Action::CooperativeCancelAccepted,
1365 Action::BuyerInvoiceAccepted,
1366 Action::BondInvoiceAccepted,
1367 Action::PurchaseCompleted,
1368 Action::BondPayoutCompleted,
1369 Action::BondSlashed,
1370 Action::HoldInvoicePaymentAccepted,
1371 Action::HoldInvoicePaymentSettled,
1372 Action::HoldInvoicePaymentCanceled,
1373 Action::WaitingSellerToPay,
1374 Action::WaitingBuyerInvoice,
1375 Action::AddInvoice,
1376 Action::BuyerTookOrder,
1377 Action::Rate,
1378 Action::RateUser,
1379 Action::RateReceived,
1380 Action::CantDo,
1381 Action::Dispute,
1382 Action::AdminCancel,
1383 Action::AdminCanceled,
1384 Action::AdminSettle,
1385 Action::AdminSettled,
1386 Action::AdminAddSolver,
1387 Action::AdminTakeDispute,
1388 Action::AdminTookDispute,
1389 Action::PaymentFailed,
1390 Action::InvoiceUpdated,
1391 Action::SendDm,
1392 Action::TradePubkey,
1393 Action::RestoreSession,
1394 Action::LastTradeIndex,
1395 Action::Orders,
1396 Action::AddCashuEscrow,
1397 Action::CashuEscrowLocked,
1398 Action::CashuPmSignature,
1399 Action::DeclarePayer,
1400 Action::PayerDeclared,
1401 Action::PaymentHistory,
1402 Action::ExportReputation,
1403 Action::ReputationExported,
1404 Action::ImportReputation,
1405 Action::ReputationImported,
1406 ];
1407
1408 for action in other_actions {
1409 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1410 order: order.clone(),
1411 slashed_at: 0,
1412 });
1413 let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1414 assert!(
1415 !kind.verify(),
1416 "BondPayoutRequest must be rejected on {action:?}"
1417 );
1418 }
1419 }
1420
1421 #[test]
1422 fn test_payer_payloads_rejected_on_unrelated_actions() {
1423 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1426 let unrelated = [
1427 Action::NewOrder,
1428 Action::TakeSell,
1429 Action::TakeBuy,
1430 Action::PayInvoice,
1431 Action::PayBondInvoice,
1432 Action::FiatSent,
1433 Action::FiatSentOk,
1434 Action::Release,
1435 Action::Released,
1436 Action::Cancel,
1437 Action::Canceled,
1438 Action::CooperativeCancelInitiatedByYou,
1439 Action::CooperativeCancelInitiatedByPeer,
1440 Action::DisputeInitiatedByYou,
1441 Action::DisputeInitiatedByPeer,
1442 Action::CooperativeCancelAccepted,
1443 Action::BuyerInvoiceAccepted,
1444 Action::BondInvoiceAccepted,
1445 Action::PurchaseCompleted,
1446 Action::BondPayoutCompleted,
1447 Action::BondSlashed,
1448 Action::HoldInvoicePaymentAccepted,
1449 Action::HoldInvoicePaymentSettled,
1450 Action::HoldInvoicePaymentCanceled,
1451 Action::WaitingSellerToPay,
1452 Action::WaitingBuyerInvoice,
1453 Action::AddInvoice,
1454 Action::BuyerTookOrder,
1455 Action::Rate,
1456 Action::RateUser,
1457 Action::RateReceived,
1458 Action::CantDo,
1459 Action::Dispute,
1460 Action::AdminCancel,
1461 Action::AdminCanceled,
1462 Action::AdminSettle,
1463 Action::AdminSettled,
1464 Action::AdminAddSolver,
1465 Action::AdminTakeDispute,
1466 Action::AdminTookDispute,
1467 Action::PaymentFailed,
1468 Action::InvoiceUpdated,
1469 Action::SendDm,
1470 Action::TradePubkey,
1471 Action::RestoreSession,
1472 Action::LastTradeIndex,
1473 Action::Orders,
1474 Action::AddCashuEscrow,
1475 Action::CashuEscrowLocked,
1476 Action::CashuPmSignature,
1477 Action::AddBondInvoice,
1478 ];
1479 for action in unrelated {
1480 for payload in [
1481 Payload::PayerDeclaration(PayerDeclaration::new("a".repeat(64))),
1482 Payload::PaymentHistory(PaymentHistory::unavailable("a".repeat(64))),
1483 ] {
1484 let kind =
1485 MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1486 assert!(!kind.verify(), "{action:?} must reject {:?}", kind.payload);
1487 }
1488 }
1489 }
1490
1491 #[test]
1492 fn test_bond_payout_ack_actions_verify_and_wire_format() {
1493 use crate::message::BondResolution;
1494
1495 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1496
1497 let order = || SmallOrder {
1500 id: Some(order_id),
1501 kind: None,
1502 status: None,
1503 amount: 500,
1504 fiat_code: "USD".to_string(),
1505 min_amount: None,
1506 max_amount: None,
1507 fiat_amount: 0,
1508 payment_method: "lightning".to_string(),
1509 premium: 0,
1510 buyer_trade_pubkey: None,
1511 seller_trade_pubkey: None,
1512 buyer_invoice: None,
1513 created_at: None,
1514 expires_at: None,
1515 cashu_mint_url: None,
1516 };
1517
1518 for (action, discriminator) in [
1524 (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1525 (Action::BondPayoutCompleted, "bond-payout-completed"),
1526 (Action::BondSlashed, "bond-slashed"),
1527 ] {
1528 let ok = Message::Order(MessageKind::new(
1530 Some(order_id),
1531 Some(1),
1532 Some(2),
1533 action.clone(),
1534 Some(Payload::Order(order())),
1535 ));
1536 assert!(ok.verify(), "{action:?} + Order should verify");
1537
1538 let no_id = Message::Order(MessageKind::new(
1540 None,
1541 Some(1),
1542 Some(2),
1543 action.clone(),
1544 Some(Payload::Order(order())),
1545 ));
1546 assert!(!no_id.verify(), "{action:?} without id must be rejected");
1547
1548 let with_resolution = Message::Order(MessageKind::new(
1550 Some(order_id),
1551 Some(1),
1552 Some(2),
1553 action.clone(),
1554 Some(Payload::BondResolution(BondResolution {
1555 slash_seller: true,
1556 slash_buyer: false,
1557 })),
1558 ));
1559 assert!(
1560 !with_resolution.verify(),
1561 "{action:?} + BondResolution must be rejected"
1562 );
1563
1564 let with_request = Message::Order(MessageKind::new(
1566 Some(order_id),
1567 Some(1),
1568 Some(2),
1569 action.clone(),
1570 Some(Payload::BondPayoutRequest(BondPayoutRequest {
1571 order: order(),
1572 slashed_at: 0,
1573 })),
1574 ));
1575 assert!(
1576 !with_request.verify(),
1577 "{action:?} + BondPayoutRequest must be rejected"
1578 );
1579
1580 let json = ok.as_json().unwrap();
1582 assert!(
1583 json.contains(&format!("\"action\":\"{discriminator}\"")),
1584 "expected kebab-case discriminator {discriminator}, got: {json}"
1585 );
1586 let decoded = Message::from_json(&json).unwrap();
1587 assert!(decoded.verify());
1588 assert_eq!(decoded.inner_action(), Some(action));
1589 }
1590 }
1591
1592 #[test]
1593 fn test_payment_failed_payload() {
1594 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1595
1596 let payment_failed_info = crate::message::PaymentFailedInfo {
1598 payment_attempts: 3,
1599 payment_retries_interval: 60,
1600 };
1601
1602 let payload = Payload::PaymentFailed(payment_failed_info);
1603 let message = Message::Order(MessageKind::new(
1604 Some(uuid),
1605 Some(1),
1606 Some(2),
1607 Action::PaymentFailed,
1608 Some(payload),
1609 ));
1610
1611 assert!(message.verify());
1613
1614 let message_json = message.as_json().unwrap();
1616
1617 let deserialized_message = Message::from_json(&message_json).unwrap();
1619 assert!(deserialized_message.verify());
1620
1621 if let Message::Order(kind) = deserialized_message {
1623 if let Some(Payload::PaymentFailed(info)) = kind.payload {
1624 assert_eq!(info.payment_attempts, 3);
1625 assert_eq!(info.payment_retries_interval, 60);
1626 } else {
1627 panic!("Expected PaymentFailed payload");
1628 }
1629 } else {
1630 panic!("Expected Order message");
1631 }
1632 }
1633
1634 #[test]
1635 fn test_message_payload_signature() {
1636 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1637 let peer = Peer::new(
1638 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1639 None, );
1641 let payload = Payload::Peer(peer);
1642 let test_message = Message::Order(MessageKind::new(
1643 Some(uuid),
1644 Some(1),
1645 Some(2),
1646 Action::FiatSentOk,
1647 Some(payload),
1648 ));
1649 assert!(test_message.verify());
1650 let test_message_json = test_message.as_json().unwrap();
1651 let trade_keys =
1653 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1654 .unwrap();
1655 let sig = Message::sign(test_message_json.clone(), &trade_keys);
1656
1657 assert!(Message::verify_signature(
1658 test_message_json,
1659 trade_keys.public_key(),
1660 sig
1661 ));
1662 }
1663
1664 #[test]
1665 fn test_restore_session_message() {
1666 let restore_request_message = Message::Restore(MessageKind::new(
1668 None,
1669 None,
1670 None,
1671 Action::RestoreSession,
1672 None,
1673 ));
1674
1675 assert!(restore_request_message.verify());
1677 assert_eq!(
1678 restore_request_message.inner_action(),
1679 Some(Action::RestoreSession)
1680 );
1681
1682 let message_json = restore_request_message.as_json().unwrap();
1684 let deserialized_message = Message::from_json(&message_json).unwrap();
1685 assert!(deserialized_message.verify());
1686 assert_eq!(
1687 deserialized_message.inner_action(),
1688 Some(Action::RestoreSession)
1689 );
1690
1691 let restored_orders = vec![
1693 crate::message::RestoredOrdersInfo {
1694 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1695 trade_index: 1,
1696 status: "active".to_string(),
1697 counterparty_trade_pubkey: Some(
1698 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1699 ),
1700 },
1701 crate::message::RestoredOrdersInfo {
1702 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1703 trade_index: 2,
1704 status: "success".to_string(),
1705 counterparty_trade_pubkey: None,
1706 },
1707 ];
1708
1709 let restored_disputes = vec![
1710 crate::message::RestoredDisputesInfo {
1711 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1712 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1713 trade_index: 1,
1714 status: "initiated".to_string(),
1715 initiator: Some(crate::message::DisputeInitiator::Buyer),
1716 solver_pubkey: None,
1717 },
1718 crate::message::RestoredDisputesInfo {
1719 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1720 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1721 trade_index: 2,
1722 status: "in-progress".to_string(),
1723 initiator: None,
1724 solver_pubkey: Some(
1725 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1726 ),
1727 },
1728 crate::message::RestoredDisputesInfo {
1729 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1730 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1731 trade_index: 3,
1732 status: "initiated".to_string(),
1733 initiator: Some(crate::message::DisputeInitiator::Seller),
1734 solver_pubkey: None,
1735 },
1736 ];
1737
1738 let restore_session_info = crate::message::RestoreSessionInfo {
1739 restore_orders: restored_orders.clone(),
1740 restore_disputes: restored_disputes.clone(),
1741 };
1742
1743 let restore_data_payload = Payload::RestoreData(restore_session_info);
1744 let restore_data_message = Message::Restore(MessageKind::new(
1745 None,
1746 None,
1747 None,
1748 Action::RestoreSession,
1749 Some(restore_data_payload),
1750 ));
1751
1752 assert!(!restore_data_message.verify());
1754
1755 let message_json = restore_data_message.as_json().unwrap();
1757 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1758
1759 if let Message::Restore(kind) = deserialized_restore_message {
1760 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1761 assert_eq!(session_info.restore_disputes.len(), 3);
1762 assert_eq!(
1763 session_info.restore_disputes[0].initiator,
1764 Some(crate::message::DisputeInitiator::Buyer)
1765 );
1766 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1767 assert_eq!(session_info.restore_disputes[1].initiator, None);
1768 assert_eq!(
1769 session_info.restore_disputes[1].solver_pubkey,
1770 Some(
1771 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1772 .to_string()
1773 )
1774 );
1775 assert_eq!(
1776 session_info.restore_disputes[2].initiator,
1777 Some(crate::message::DisputeInitiator::Seller)
1778 );
1779 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1780 assert_eq!(
1781 session_info.restore_orders[0].counterparty_trade_pubkey,
1782 Some(
1783 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1784 .to_string()
1785 )
1786 );
1787 assert!(session_info.restore_orders[1]
1788 .counterparty_trade_pubkey
1789 .is_none());
1790 } else {
1791 panic!("Expected RestoreData payload");
1792 }
1793 } else {
1794 panic!("Expected Restore message");
1795 }
1796 }
1797
1798 #[test]
1801 fn restored_order_without_counterparty_pubkey_still_parses() {
1802 let json = r#"{
1803 "order_id": "308e1272-d5f4-47e6-bd97-3504baea9c23",
1804 "trade_index": 1,
1805 "status": "active"
1806 }"#;
1807
1808 let order: crate::message::RestoredOrdersInfo = serde_json::from_str(json).unwrap();
1809
1810 assert_eq!(order.trade_index, 1);
1811 assert_eq!(order.status, "active");
1812 assert!(order.counterparty_trade_pubkey.is_none());
1813 }
1814
1815 #[test]
1816 fn test_restore_session_message_validation() {
1817 let restore_request_message = Message::Restore(MessageKind::new(
1819 None,
1820 None,
1821 None,
1822 Action::RestoreSession,
1823 None, ));
1825
1826 assert!(restore_request_message.verify());
1828
1829 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1831 let wrong_message = Message::Restore(MessageKind::new(
1832 None,
1833 None,
1834 None,
1835 Action::RestoreSession,
1836 Some(wrong_payload),
1837 ));
1838
1839 assert!(!wrong_message.verify());
1841
1842 let with_id = Message::Restore(MessageKind::new(
1844 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1845 None,
1846 None,
1847 Action::RestoreSession,
1848 None,
1849 ));
1850 assert!(with_id.verify());
1851
1852 let with_request_id = Message::Restore(MessageKind::new(
1853 None,
1854 Some(42),
1855 None,
1856 Action::RestoreSession,
1857 None,
1858 ));
1859 assert!(with_request_id.verify());
1860
1861 let with_trade_index = Message::Restore(MessageKind::new(
1862 None,
1863 None,
1864 Some(7),
1865 Action::RestoreSession,
1866 None,
1867 ));
1868 assert!(with_trade_index.verify());
1869 }
1870
1871 #[test]
1872 fn test_restore_session_message_constructor() {
1873 let restore_request_message = Message::new_restore(None);
1875
1876 assert!(matches!(restore_request_message, Message::Restore(_)));
1877 assert!(restore_request_message.verify());
1878 assert_eq!(
1879 restore_request_message.inner_action(),
1880 Some(Action::RestoreSession)
1881 );
1882
1883 let restore_session_info = crate::message::RestoreSessionInfo {
1885 restore_orders: vec![],
1886 restore_disputes: vec![],
1887 };
1888 let restore_data_message =
1889 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1890
1891 assert!(matches!(restore_data_message, Message::Restore(_)));
1892 assert!(!restore_data_message.verify());
1893 }
1894
1895 #[test]
1896 fn test_last_trade_index_valid_message() {
1897 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1898 let msg = Message::Restore(kind);
1899
1900 assert!(msg.verify());
1901
1902 let json = msg.as_json().unwrap();
1904 let decoded = Message::from_json(&json).unwrap();
1905 assert!(decoded.verify());
1906
1907 let inner = decoded.get_inner_message_kind();
1909 assert_eq!(inner.trade_index(), 7);
1910 assert_eq!(inner.has_trade_index(), (true, 7));
1911 }
1912
1913 #[test]
1914 fn test_last_trade_index_without_id_is_valid() {
1915 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1917 let msg = Message::Restore(kind);
1918 assert!(msg.verify());
1919 }
1920
1921 #[test]
1922 fn test_last_trade_index_with_payload_fails_validation() {
1923 let kind = MessageKind::new(
1925 None,
1926 None,
1927 Some(3),
1928 Action::LastTradeIndex,
1929 Some(Payload::TextMessage("ignored".to_string())),
1930 );
1931 let msg = Message::Restore(kind);
1932 assert!(!msg.verify());
1933 }
1934
1935 #[test]
1936 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1937 use crate::message::BondResolution;
1938
1939 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1940
1941 for action in [Action::AdminSettle, Action::AdminCancel] {
1942 let with_resolution = Message::Order(MessageKind::new(
1943 Some(uuid),
1944 Some(1),
1945 Some(2),
1946 action.clone(),
1947 Some(Payload::BondResolution(BondResolution {
1948 slash_seller: true,
1949 slash_buyer: false,
1950 })),
1951 ));
1952 assert!(
1953 with_resolution.verify(),
1954 "{action:?} + BondResolution should verify"
1955 );
1956
1957 let without_payload = Message::Order(MessageKind::new(
1958 Some(uuid),
1959 Some(1),
1960 Some(2),
1961 action.clone(),
1962 None,
1963 ));
1964 assert!(without_payload.verify(), "{action:?} + None should verify");
1965
1966 let wrong = Message::Order(MessageKind::new(
1968 Some(uuid),
1969 Some(1),
1970 Some(2),
1971 action.clone(),
1972 Some(Payload::TextMessage("nope".to_string())),
1973 ));
1974 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1975
1976 let no_id = Message::Order(MessageKind::new(
1978 None,
1979 Some(1),
1980 Some(2),
1981 action,
1982 Some(Payload::BondResolution(BondResolution {
1983 slash_seller: false,
1984 slash_buyer: false,
1985 })),
1986 ));
1987 assert!(!no_id.verify(), "admin action without id must be rejected");
1988 }
1989 }
1990
1991 #[test]
1992 fn test_bond_resolution_rejected_on_non_admin_actions() {
1993 use crate::message::BondResolution;
1994
1995 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1996 let payload = Payload::BondResolution(BondResolution {
1997 slash_seller: true,
1998 slash_buyer: true,
1999 });
2000
2001 for action in [
2005 Action::NewOrder,
2006 Action::TakeSell,
2007 Action::TakeBuy,
2008 Action::PayInvoice,
2009 Action::PayBondInvoice,
2010 Action::FiatSent,
2011 Action::FiatSentOk,
2012 Action::Release,
2013 Action::Released,
2014 Action::Cancel,
2015 Action::Canceled,
2016 Action::CooperativeCancelInitiatedByYou,
2017 Action::CooperativeCancelInitiatedByPeer,
2018 Action::DisputeInitiatedByYou,
2019 Action::DisputeInitiatedByPeer,
2020 Action::CooperativeCancelAccepted,
2021 Action::BuyerInvoiceAccepted,
2022 Action::BondInvoiceAccepted,
2023 Action::PurchaseCompleted,
2024 Action::BondPayoutCompleted,
2025 Action::BondSlashed,
2026 Action::HoldInvoicePaymentAccepted,
2027 Action::HoldInvoicePaymentSettled,
2028 Action::HoldInvoicePaymentCanceled,
2029 Action::WaitingSellerToPay,
2030 Action::WaitingBuyerInvoice,
2031 Action::AddInvoice,
2032 Action::AddBondInvoice,
2033 Action::BuyerTookOrder,
2034 Action::Rate,
2035 Action::RateUser,
2036 Action::RateReceived,
2037 Action::CantDo,
2038 Action::Dispute,
2039 Action::AdminCanceled,
2040 Action::AdminSettled,
2041 Action::AdminAddSolver,
2042 Action::AdminTakeDispute,
2043 Action::AdminTookDispute,
2044 Action::PaymentFailed,
2045 Action::InvoiceUpdated,
2046 Action::SendDm,
2047 Action::TradePubkey,
2048 Action::RestoreSession,
2049 Action::LastTradeIndex,
2050 Action::Orders,
2051 ] {
2052 let msg = Message::Order(MessageKind::new(
2053 Some(uuid),
2054 Some(1),
2055 Some(2),
2056 action.clone(),
2057 Some(payload.clone()),
2058 ));
2059 assert!(
2060 !msg.verify(),
2061 "{action:?} must reject BondResolution payload"
2062 );
2063 }
2064 }
2065
2066 #[test]
2067 fn test_bond_resolution_wire_format() {
2068 use crate::message::BondResolution;
2069
2070 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2071 let msg = Message::Order(MessageKind::new(
2072 Some(uuid),
2073 None,
2074 None,
2075 Action::AdminCancel,
2076 Some(Payload::BondResolution(BondResolution {
2077 slash_seller: true,
2078 slash_buyer: false,
2079 })),
2080 ));
2081
2082 let json = msg.as_json().unwrap();
2083 assert!(
2085 json.contains("\"bond_resolution\""),
2086 "expected snake_case discriminator, got: {json}"
2087 );
2088 assert!(json.contains("\"slash_seller\":true"));
2089 assert!(json.contains("\"slash_buyer\":false"));
2090
2091 let decoded = Message::from_json(&json).unwrap();
2093 assert!(decoded.verify());
2094 if let Message::Order(kind) = decoded {
2095 match kind.payload {
2096 Some(Payload::BondResolution(b)) => {
2097 assert!(b.slash_seller);
2098 assert!(!b.slash_buyer);
2099 }
2100 other => panic!("expected BondResolution payload, got {other:?}"),
2101 }
2102 } else {
2103 panic!("expected Order message");
2104 }
2105 }
2106
2107 #[test]
2108 fn test_bond_resolution_legacy_null_payload() {
2109 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2113 let json = format!(
2114 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
2115 );
2116 let msg = Message::from_json(&json).unwrap();
2117 assert!(msg.verify());
2118 }
2119
2120 #[test]
2121 fn test_pay_bond_invoice_wire_format_and_verify() {
2122 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2123 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
2124
2125 let msg = Message::Order(MessageKind::new(
2126 Some(uuid),
2127 Some(1),
2128 Some(2),
2129 Action::PayBondInvoice,
2130 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2131 ));
2132 assert!(msg.verify());
2133
2134 let json = msg.as_json().unwrap();
2136 assert!(
2137 json.contains("\"action\":\"pay-bond-invoice\""),
2138 "expected kebab-case discriminator, got: {json}"
2139 );
2140
2141 let decoded = Message::from_json(&json).unwrap();
2143 assert!(decoded.verify());
2144 assert!(matches!(
2145 decoded.inner_action(),
2146 Some(Action::PayBondInvoice)
2147 ));
2148
2149 let no_id = Message::Order(MessageKind::new(
2151 None,
2152 Some(1),
2153 Some(2),
2154 Action::PayBondInvoice,
2155 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2156 ));
2157 assert!(!no_id.verify());
2158
2159 let wrong_payload = Message::Order(MessageKind::new(
2161 Some(uuid),
2162 Some(1),
2163 Some(2),
2164 Action::PayBondInvoice,
2165 Some(Payload::TextMessage("nope".to_string())),
2166 ));
2167 assert!(!wrong_payload.verify());
2168
2169 let no_payload = Message::Order(MessageKind::new(
2171 Some(uuid),
2172 Some(1),
2173 Some(2),
2174 Action::PayBondInvoice,
2175 None,
2176 ));
2177 assert!(!no_payload.verify());
2178 }
2179
2180 #[test]
2181 fn test_add_bond_invoice_wire_format_and_verify() {
2182 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2183 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
2184
2185 let msg = Message::Order(MessageKind::new(
2186 Some(uuid),
2187 Some(1),
2188 Some(2),
2189 Action::AddBondInvoice,
2190 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2191 ));
2192 assert!(msg.verify());
2193
2194 let json = msg.as_json().unwrap();
2196 assert!(
2197 json.contains("\"action\":\"add-bond-invoice\""),
2198 "expected kebab-case discriminator, got: {json}"
2199 );
2200
2201 let decoded = Message::from_json(&json).unwrap();
2203 assert!(decoded.verify());
2204 assert!(matches!(
2205 decoded.inner_action(),
2206 Some(Action::AddBondInvoice)
2207 ));
2208
2209 let no_id = Message::Order(MessageKind::new(
2211 None,
2212 Some(1),
2213 Some(2),
2214 Action::AddBondInvoice,
2215 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2216 ));
2217 assert!(!no_id.verify());
2218
2219 let wrong_payload = Message::Order(MessageKind::new(
2221 Some(uuid),
2222 Some(1),
2223 Some(2),
2224 Action::AddBondInvoice,
2225 Some(Payload::TextMessage("nope".to_string())),
2226 ));
2227 assert!(!wrong_payload.verify());
2228
2229 let no_payload = Message::Order(MessageKind::new(
2231 Some(uuid),
2232 Some(1),
2233 Some(2),
2234 Action::AddBondInvoice,
2235 None,
2236 ));
2237 assert!(!no_payload.verify());
2238
2239 if let Message::Order(kind) = &msg {
2241 assert_eq!(kind.get_payment_request(), Some(bolt11));
2242 } else {
2243 panic!("expected Message::Order");
2244 }
2245 }
2246
2247 #[test]
2248 fn test_restored_dispute_helper_serialization_roundtrip() {
2249 use crate::message::RestoredDisputeHelper;
2250
2251 let helper = RestoredDisputeHelper {
2252 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
2253 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
2254 dispute_status: "initiated".to_string(),
2255 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
2256 master_seller_pubkey: Some("npub1sellerkey".to_string()),
2257 trade_index_buyer: Some(1),
2258 trade_index_seller: Some(2),
2259 buyer_dispute: true,
2260 seller_dispute: false,
2261 solver_pubkey: None,
2262 };
2263
2264 let json = serde_json::to_string(&helper).unwrap();
2265 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
2266
2267 assert_eq!(deserialized.dispute_id, helper.dispute_id);
2268 assert_eq!(deserialized.order_id, helper.order_id);
2269 assert_eq!(deserialized.dispute_status, helper.dispute_status);
2270 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
2271 assert_eq!(
2272 deserialized.master_seller_pubkey,
2273 helper.master_seller_pubkey
2274 );
2275 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
2276 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
2277 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
2278 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
2279 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
2280
2281 let helper_seller_dispute = RestoredDisputeHelper {
2282 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
2283 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
2284 dispute_status: "in-progress".to_string(),
2285 master_buyer_pubkey: None,
2286 master_seller_pubkey: None,
2287 trade_index_buyer: None,
2288 trade_index_seller: None,
2289 buyer_dispute: false,
2290 seller_dispute: true,
2291 solver_pubkey: Some(
2292 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
2293 ),
2294 };
2295
2296 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
2297 let deserialized_seller: RestoredDisputeHelper =
2298 serde_json::from_str(&json_seller).unwrap();
2299
2300 assert_eq!(
2301 deserialized_seller.dispute_id,
2302 helper_seller_dispute.dispute_id
2303 );
2304 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
2305 assert_eq!(
2306 deserialized_seller.dispute_status,
2307 helper_seller_dispute.dispute_status
2308 );
2309 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
2310 assert_eq!(deserialized_seller.master_seller_pubkey, None);
2311 assert_eq!(deserialized_seller.trade_index_buyer, None);
2312 assert_eq!(deserialized_seller.trade_index_seller, None);
2313 assert!(!deserialized_seller.buyer_dispute);
2314 assert!(deserialized_seller.seller_dispute);
2315 assert_eq!(
2316 deserialized_seller.solver_pubkey,
2317 helper_seller_dispute.solver_pubkey
2318 );
2319 }
2320
2321 fn sample_lock_proof() -> CashuLockProof {
2322 CashuLockProof::new(
2323 "cashuAeyJ0b2tlbiI6dGVzdA".to_string(),
2324 "https://mint.example".to_string(),
2325 "02b_buyer".to_string(),
2326 "02s_seller".to_string(),
2327 "02m_mostro".to_string(),
2328 )
2329 }
2330
2331 #[test]
2332 fn test_cashu_lock_proof_json_round_trip() {
2333 let proof = sample_lock_proof();
2334 let json = proof.as_json().unwrap();
2335 let back = CashuLockProof::from_json(&json).unwrap();
2336 assert_eq!(back, proof);
2337 }
2338
2339 #[test]
2340 fn test_cashu_lock_proof_fee_token_round_trip() {
2341 let proof = sample_lock_proof().with_fee_token("cashuAfee".to_string());
2342 let json = proof.as_json().unwrap();
2343 assert!(json.contains("fee_token"));
2344 let back = CashuLockProof::from_json(&json).unwrap();
2345 assert_eq!(back, proof);
2346 assert_eq!(back.fee_token.as_deref(), Some("cashuAfee"));
2347 }
2348
2349 #[test]
2350 fn test_cashu_lock_proof_without_fee_token_is_omitted_and_defaults_to_none() {
2351 let proof = sample_lock_proof();
2354 assert_eq!(proof.fee_token, None);
2355 let json = proof.as_json().unwrap();
2356 assert!(!json.contains("fee_token"));
2357 let back = CashuLockProof::from_json(&json).unwrap();
2358 assert_eq!(back.fee_token, None);
2359 }
2360
2361 #[test]
2362 fn test_add_cashu_escrow_verifies_with_lock_proof() {
2363 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2364 let payload = Payload::CashuLockProof(sample_lock_proof());
2365 let kind = MessageKind::new(
2366 Some(order_id),
2367 None,
2368 None,
2369 Action::AddCashuEscrow,
2370 Some(payload),
2371 );
2372 assert!(
2373 kind.verify(),
2374 "CashuLockProof must verify on AddCashuEscrow"
2375 );
2376
2377 let json = Message::Order(kind).as_json().unwrap();
2380 assert!(json.contains("cashu_lock_proof"));
2381 assert!(Message::from_json(&json).unwrap().verify());
2382 }
2383
2384 #[test]
2385 fn test_add_cashu_escrow_requires_id_and_right_payload() {
2386 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2387
2388 let no_id = MessageKind::new(
2390 None,
2391 None,
2392 None,
2393 Action::AddCashuEscrow,
2394 Some(Payload::CashuLockProof(sample_lock_proof())),
2395 );
2396 assert!(
2397 !no_id.verify(),
2398 "AddCashuEscrow without id must be rejected"
2399 );
2400
2401 let wrong_payload = MessageKind::new(
2403 Some(order_id),
2404 None,
2405 None,
2406 Action::AddCashuEscrow,
2407 Some(Payload::TextMessage("not a lock proof".to_string())),
2408 );
2409 assert!(
2410 !wrong_payload.verify(),
2411 "AddCashuEscrow with non-lock-proof payload must be rejected"
2412 );
2413 }
2414
2415 #[test]
2416 fn test_cashu_pm_signature_verifies_with_signatures() {
2417 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2418 let kind = MessageKind::new(
2419 Some(order_id),
2420 None,
2421 None,
2422 Action::CashuPmSignature,
2423 Some(Payload::CashuSignatures(vec![
2424 CashuProofSignature::new("secret-0".to_string(), "deadbeef".to_string()),
2425 CashuProofSignature::new("secret-1".to_string(), "c0ffee".to_string()),
2426 ])),
2427 );
2428 assert!(
2429 kind.verify(),
2430 "CashuSignatures must verify on CashuPmSignature"
2431 );
2432
2433 let json = Message::Order(kind).as_json().unwrap();
2434 assert!(json.contains("cashu_signatures"));
2435 assert!(Message::from_json(&json).unwrap().verify());
2436
2437 let wrong = MessageKind::new(Some(order_id), None, None, Action::CashuPmSignature, None);
2439 assert!(
2440 !wrong.verify(),
2441 "CashuPmSignature without a signature payload must be rejected"
2442 );
2443
2444 let empty = MessageKind::new(
2447 Some(order_id),
2448 None,
2449 None,
2450 Action::CashuPmSignature,
2451 Some(Payload::CashuSignatures(vec![])),
2452 );
2453 assert!(
2454 !empty.verify(),
2455 "CashuPmSignature with an empty signature set must be rejected"
2456 );
2457 }
2458
2459 #[test]
2460 fn test_cashu_escrow_locked_is_informational() {
2461 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2462
2463 let ok = MessageKind::new(Some(order_id), None, None, Action::CashuEscrowLocked, None);
2465 assert!(ok.verify(), "CashuEscrowLocked with id must verify");
2466
2467 let no_id = MessageKind::new(None, None, None, Action::CashuEscrowLocked, None);
2469 assert!(
2470 !no_id.verify(),
2471 "CashuEscrowLocked without id must be rejected"
2472 );
2473 }
2474
2475 fn sample_declaration() -> PayerDeclaration {
2476 PayerDeclaration::new(payment_hash("EU|SEPA|DE89370400440532013000|ALICE SMITH"))
2477 }
2478
2479 fn sample_history() -> PaymentHistory {
2480 PaymentHistory {
2481 payment_hash: sample_declaration().payment_hash,
2482 buyer_mode: BuyerMode::Reputation,
2483 successful_trades: 47,
2484 distinct_counterparties: 29,
2485 experienced_counterparties: 11,
2486 first_success_at: Some(1_762_128_000),
2487 last_success_at: Some(1_787_654_321),
2488 }
2489 }
2490
2491 #[test]
2492 fn test_payer_history_actions_use_kebab_case_wire_names() {
2493 for (action, wire) in [
2494 (Action::DeclarePayer, "\"declare-payer\""),
2495 (Action::PayerDeclared, "\"payer-declared\""),
2496 (Action::PaymentHistory, "\"payment-history\""),
2497 ] {
2498 assert_eq!(serde_json::to_string(&action).unwrap(), wire);
2499 assert_eq!(serde_json::from_str::<Action>(wire).unwrap(), action);
2500 }
2501 }
2502
2503 #[test]
2504 fn test_declare_payer_requires_id_and_declaration() {
2505 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2506 let decl = Some(Payload::PayerDeclaration(sample_declaration()));
2507
2508 let ok = MessageKind::new(
2509 Some(order_id),
2510 Some(1),
2511 Some(7),
2512 Action::DeclarePayer,
2513 decl.clone(),
2514 );
2515 assert!(
2516 ok.verify(),
2517 "DeclarePayer with id and declaration must verify"
2518 );
2519
2520 let no_id = MessageKind::new(None, None, None, Action::DeclarePayer, decl);
2521 assert!(!no_id.verify(), "DeclarePayer without id must be rejected");
2522
2523 let no_payload = MessageKind::new(Some(order_id), None, None, Action::DeclarePayer, None);
2524 assert!(
2525 !no_payload.verify(),
2526 "DeclarePayer without payload must be rejected"
2527 );
2528
2529 let wrong = MessageKind::new(
2530 Some(order_id),
2531 None,
2532 None,
2533 Action::DeclarePayer,
2534 Some(Payload::TextMessage(
2535 "plaintext must never be sent".to_string(),
2536 )),
2537 );
2538 assert!(
2539 !wrong.verify(),
2540 "DeclarePayer with a non-declaration payload must be rejected"
2541 );
2542 }
2543
2544 #[test]
2545 fn test_payer_declared_requires_id_and_declaration() {
2546 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2547 let decl = Some(Payload::PayerDeclaration(sample_declaration()));
2548
2549 let ok = MessageKind::new(
2550 Some(order_id),
2551 None,
2552 None,
2553 Action::PayerDeclared,
2554 decl.clone(),
2555 );
2556 assert!(
2557 ok.verify(),
2558 "PayerDeclared with id and declaration must verify"
2559 );
2560
2561 let no_id = MessageKind::new(None, None, None, Action::PayerDeclared, decl);
2562 assert!(!no_id.verify(), "PayerDeclared without id must be rejected");
2563
2564 let no_payload = MessageKind::new(Some(order_id), None, None, Action::PayerDeclared, None);
2565 assert!(
2566 !no_payload.verify(),
2567 "PayerDeclared without payload must be rejected"
2568 );
2569
2570 let wrong = MessageKind::new(
2571 Some(order_id),
2572 None,
2573 None,
2574 Action::PayerDeclared,
2575 Some(Payload::PaymentHistory(sample_history())),
2576 );
2577 assert!(
2578 !wrong.verify(),
2579 "PayerDeclared with a history payload must be rejected"
2580 );
2581 }
2582
2583 #[test]
2584 fn test_payment_history_accepts_query_and_reply_shapes() {
2585 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2586
2587 let query = MessageKind::new(Some(order_id), Some(3), None, Action::PaymentHistory, None);
2589 assert!(
2590 query.verify(),
2591 "PaymentHistory query (no payload) must verify"
2592 );
2593
2594 let reply = MessageKind::new(
2596 Some(order_id),
2597 None,
2598 None,
2599 Action::PaymentHistory,
2600 Some(Payload::PaymentHistory(sample_history())),
2601 );
2602 assert!(reply.verify(), "PaymentHistory reply must verify");
2603
2604 let no_id = MessageKind::new(None, None, None, Action::PaymentHistory, None);
2605 assert!(
2606 !no_id.verify(),
2607 "PaymentHistory without id must be rejected"
2608 );
2609
2610 let wrong = MessageKind::new(
2611 Some(order_id),
2612 None,
2613 None,
2614 Action::PaymentHistory,
2615 Some(Payload::PayerDeclaration(sample_declaration())),
2616 );
2617 assert!(
2618 !wrong.verify(),
2619 "PaymentHistory with a declaration payload must be rejected"
2620 );
2621 }
2622
2623 #[test]
2624 fn test_payer_payloads_round_trip_with_snake_case_keys() {
2625 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2626
2627 let declare = Message::Order(MessageKind::new(
2628 Some(order_id),
2629 Some(981_231),
2630 Some(7),
2631 Action::DeclarePayer,
2632 Some(Payload::PayerDeclaration(sample_declaration())),
2633 ));
2634 let json = declare.as_json().unwrap();
2635 assert!(json.contains("\"action\":\"declare-payer\""));
2636 assert!(json.contains("\"payer_declaration\":{\"payment_hash\":"));
2637 assert!(Message::from_json(&json).unwrap().verify());
2638
2639 let push = Message::Order(MessageKind::new(
2640 Some(order_id),
2641 None,
2642 None,
2643 Action::PaymentHistory,
2644 Some(Payload::PaymentHistory(sample_history())),
2645 ));
2646 let json = push.as_json().unwrap();
2647 assert!(json.contains("\"payment_history\":{"));
2648 assert!(json.contains("\"buyer_mode\":\"reputation\""));
2649 assert!(json.contains("\"experienced_counterparties\":11"));
2650 let back = Message::from_json(&json).unwrap();
2651 assert!(back.verify());
2652 match back.get_inner_message_kind().payload.clone() {
2653 Some(Payload::PaymentHistory(h)) => assert_eq!(h, sample_history()),
2654 other => panic!("expected a PaymentHistory payload, got {other:?}"),
2655 }
2656 }
2657
2658 #[test]
2659 fn test_payment_history_parses_the_protocol_wire_example() {
2660 let hash = sample_declaration().payment_hash;
2663 let json = format!(
2664 r#"{{"payment_history": {{"payment_hash": "{hash}", "buyer_mode": "full_privacy",
2665 "successful_trades": 0, "distinct_counterparties": 0,
2666 "experienced_counterparties": 0,
2667 "first_success_at": null, "last_success_at": null}}}}"#
2668 );
2669 let payload: Payload = serde_json::from_str(&json).unwrap();
2670 match payload {
2671 Payload::PaymentHistory(h) => assert_eq!(h, PaymentHistory::unavailable(hash)),
2672 other => panic!("expected a PaymentHistory payload, got {other:?}"),
2673 }
2674 }
2675
2676 #[test]
2679 fn reputation_messages_have_the_documented_wire_shape() {
2680 let export = Message::new_order(
2681 None,
2682 Some(4126),
2683 None,
2684 Action::ExportReputation,
2685 Some(Payload::ReputationExportRequest(
2686 super::ReputationExportRequest {
2687 destination: "ab".repeat(32),
2688 rebind: None,
2689 },
2690 )),
2691 );
2692 let json: serde_json::Value = serde_json::from_str(&export.as_json().unwrap()).unwrap();
2693 assert_eq!(
2694 json,
2695 serde_json::json!({ "order": {
2696 "version": 2,
2697 "request_id": 4126,
2698 "trade_index": null,
2699 "action": "export-reputation",
2700 "payload": { "reputation_export_request": {
2701 "destination": "ab".repeat(32),
2702 "rebind": null,
2703 }},
2704 }})
2705 );
2706
2707 for (action, wire) in [
2708 (Action::ReputationExported, "reputation-exported"),
2709 (Action::ImportReputation, "import-reputation"),
2710 ] {
2711 let message = Message::new_order(
2712 None,
2713 None,
2714 None,
2715 action,
2716 Some(Payload::ReputationAttestation("{}".into())),
2717 );
2718 let json: serde_json::Value =
2719 serde_json::from_str(&message.as_json().unwrap()).unwrap();
2720 assert_eq!(json["order"]["action"], wire);
2721 assert_eq!(json["order"]["payload"]["reputation_attestation"], "{}");
2722 }
2723
2724 let imported = Message::new_order(None, None, None, Action::ReputationImported, None);
2725 let json: serde_json::Value = serde_json::from_str(&imported.as_json().unwrap()).unwrap();
2726 assert_eq!(json["order"]["action"], "reputation-imported");
2727 assert!(json["order"]["payload"].is_null());
2728 }
2729
2730 #[test]
2731 fn an_export_request_without_rebind_deserialises() {
2732 let json = r#"{"order":{"version":2,"action":"export-reputation","payload":{"reputation_export_request":{"destination":"ab"}}}}"#;
2733 let message = Message::from_json(json).unwrap();
2734 let kind = message.get_inner_message_kind();
2735 match &kind.payload {
2736 Some(Payload::ReputationExportRequest(request)) => assert_eq!(request.rebind, None),
2737 other => panic!("unexpected payload {other:?}"),
2738 }
2739 assert!(kind.verify());
2740 }
2741
2742 #[test]
2743 fn reputation_actions_verify_only_without_an_id_and_with_their_payload() {
2744 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2745 let request = Payload::ReputationExportRequest(super::ReputationExportRequest {
2746 destination: "ab".repeat(32),
2747 rebind: None,
2748 });
2749 let attestation = Payload::ReputationAttestation("{}".into());
2750 let cases = [
2751 (Action::ExportReputation, Some(request.clone()), true),
2752 (Action::ExportReputation, Some(attestation.clone()), false),
2753 (Action::ExportReputation, None, false),
2754 (Action::ReputationExported, Some(attestation.clone()), true),
2755 (Action::ReputationExported, None, false),
2756 (Action::ImportReputation, Some(attestation.clone()), true),
2757 (Action::ImportReputation, Some(request.clone()), false),
2758 (Action::ReputationImported, None, true),
2759 (Action::ReputationImported, Some(attestation.clone()), false),
2760 ];
2761 for (action, payload, ok) in cases {
2762 let without_id = MessageKind::new(None, None, None, action.clone(), payload.clone());
2763 assert_eq!(without_id.verify(), ok, "{action:?} {payload:?}");
2764 let with_id = MessageKind::new(Some(order_id), None, None, action.clone(), payload);
2765 assert!(!with_id.verify(), "{action:?} must not carry an id");
2766 }
2767 }
2768
2769 #[test]
2776 fn reputation_messages_keep_protocol_version_2() {
2777 for action in [
2778 Action::ExportReputation,
2779 Action::ReputationExported,
2780 Action::ImportReputation,
2781 Action::ReputationImported,
2782 ] {
2783 assert_eq!(MessageKind::new(None, None, None, action, None).version, 2);
2784 }
2785 }
2786}