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 UserInfo,
193 Orders,
196 AddCashuEscrow,
201 CashuEscrowLocked,
206 CashuPmSignature,
212 DeclarePayer,
215 PayerDeclared,
219 PaymentHistory,
224 ExportReputation,
229 ReputationExported,
233 ImportReputation,
237 ReputationImported,
241}
242
243impl fmt::Display for Action {
244 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
245 write!(f, "{self:?}")
246 }
247}
248
249#[derive(Debug, Clone, Deserialize, Serialize)]
256#[serde(rename_all = "kebab-case")]
257pub enum Message {
258 Order(MessageKind),
260 Dispute(MessageKind),
262 CantDo(MessageKind),
264 Rate(MessageKind),
266 Dm(MessageKind),
268 Restore(MessageKind),
270}
271
272impl Message {
273 pub fn new_order(
276 id: Option<Uuid>,
277 request_id: Option<u64>,
278 trade_index: Option<i64>,
279 action: Action,
280 payload: Option<Payload>,
281 ) -> Self {
282 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
283 Self::Order(kind)
284 }
285
286 pub fn new_dispute(
289 id: Option<Uuid>,
290 request_id: Option<u64>,
291 trade_index: Option<i64>,
292 action: Action,
293 payload: Option<Payload>,
294 ) -> Self {
295 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
296
297 Self::Dispute(kind)
298 }
299
300 pub fn new_restore(payload: Option<Payload>) -> Self {
305 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
306 Self::Restore(kind)
307 }
308
309 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
312 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
313
314 Self::CantDo(kind)
315 }
316
317 pub fn new_dm(
319 id: Option<Uuid>,
320 request_id: Option<u64>,
321 action: Action,
322 payload: Option<Payload>,
323 ) -> Self {
324 let kind = MessageKind::new(id, request_id, None, action, payload);
325
326 Self::Dm(kind)
327 }
328
329 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
331 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
332 }
333
334 pub fn as_json(&self) -> Result<String, ServiceError> {
336 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
337 }
338
339 pub fn get_inner_message_kind(&self) -> &MessageKind {
341 match self {
342 Message::Dispute(k)
343 | Message::Order(k)
344 | Message::CantDo(k)
345 | Message::Rate(k)
346 | Message::Dm(k)
347 | Message::Restore(k) => k,
348 }
349 }
350
351 pub fn inner_action(&self) -> Option<Action> {
356 match self {
357 Message::Dispute(a)
358 | Message::Order(a)
359 | Message::CantDo(a)
360 | Message::Rate(a)
361 | Message::Dm(a)
362 | Message::Restore(a) => Some(a.get_action()),
363 }
364 }
365
366 pub fn verify(&self) -> bool {
370 if !matches!(self, Message::Restore(_))
371 && self.get_inner_message_kind().action == Action::UserInfo
372 {
373 return false;
374 }
375 match self {
376 Message::Order(m)
377 | Message::Dispute(m)
378 | Message::CantDo(m)
379 | Message::Rate(m)
380 | Message::Dm(m)
381 | Message::Restore(m) => m.verify(),
382 }
383 }
384
385 pub fn sign(message: String, keys: &Keys) -> Signature {
397 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
398 keys.sign_schnorr(hash.to_byte_array())
399 }
400
401 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
411 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
412 let hash = hash.to_byte_array();
413
414 let secp = Secp256k1::verification_only();
415 if let Ok(xonlykey) = pubkey.xonly() {
416 let sig = schnorr::Signature::from_byte_array(*sig.as_bytes());
417 xonlykey.verify(&secp, &hash, &sig).is_ok()
418 } else {
419 false
420 }
421 }
422}
423
424#[derive(Debug, Clone, Deserialize, Serialize)]
431pub struct MessageKind {
432 pub version: u8,
435 pub request_id: Option<u64>,
438 pub trade_index: Option<i64>,
441 #[serde(skip_serializing_if = "Option::is_none")]
444 pub id: Option<Uuid>,
445 pub action: Action,
447 pub payload: Option<Payload>,
450}
451
452type Amount = i64;
454
455#[derive(Debug, Deserialize, Serialize, Clone)]
460pub struct PaymentFailedInfo {
461 pub payment_attempts: u32,
463 pub payment_retries_interval: u32,
465}
466
467#[cfg_attr(feature = "sqlx", derive(FromRow))]
472#[derive(Debug, Deserialize, Serialize, Clone)]
473pub struct RestoredOrderHelper {
474 pub id: Uuid,
476 pub status: String,
478 pub master_buyer_pubkey: Option<String>,
480 pub master_seller_pubkey: Option<String>,
482 pub trade_index_buyer: Option<i64>,
484 pub trade_index_seller: Option<i64>,
486}
487
488#[cfg_attr(feature = "sqlx", derive(FromRow))]
493#[derive(Debug, Deserialize, Serialize, Clone)]
494pub struct RestoredDisputeHelper {
495 pub dispute_id: Uuid,
497 pub order_id: Uuid,
499 pub dispute_status: String,
501 pub master_buyer_pubkey: Option<String>,
503 pub master_seller_pubkey: Option<String>,
505 pub trade_index_buyer: Option<i64>,
507 pub trade_index_seller: Option<i64>,
509 pub buyer_dispute: bool,
513 pub seller_dispute: bool,
517 pub solver_pubkey: Option<String>,
520}
521
522#[cfg_attr(feature = "sqlx", derive(FromRow))]
524#[derive(Debug, Deserialize, Serialize, Clone)]
525pub struct RestoredOrdersInfo {
526 pub order_id: Uuid,
528 pub trade_index: i64,
530 pub status: String,
532 #[serde(default)]
546 pub counterparty_trade_pubkey: Option<String>,
547}
548
549#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
551#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
552#[serde(rename_all = "lowercase")]
553#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
554pub enum DisputeInitiator {
555 Buyer,
557 Seller,
559}
560
561#[cfg_attr(feature = "sqlx", derive(FromRow))]
563#[derive(Debug, Deserialize, Serialize, Clone)]
564pub struct RestoredDisputesInfo {
565 pub dispute_id: Uuid,
567 pub order_id: Uuid,
569 pub trade_index: i64,
571 pub status: String,
573 pub initiator: Option<DisputeInitiator>,
576 pub solver_pubkey: Option<String>,
579}
580
581#[derive(Debug, Deserialize, Serialize, Clone, Default)]
586pub struct RestoreSessionInfo {
587 #[serde(rename = "orders")]
589 pub restore_orders: Vec<RestoredOrdersInfo>,
590 #[serde(rename = "disputes")]
592 pub restore_disputes: Vec<RestoredDisputesInfo>,
593}
594
595#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
603pub struct BondResolution {
604 pub slash_seller: bool,
606 pub slash_buyer: bool,
608}
609
610#[derive(Debug, Deserialize, Serialize, Clone)]
625pub struct BondPayoutRequest {
626 pub order: SmallOrder,
630 pub slashed_at: i64,
635}
636
637#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
642pub struct ReputationExportRequest {
643 pub destination: String,
645 #[serde(default)]
650 pub rebind: Option<String>,
651}
652
653#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
662pub struct CashuLockProof {
663 pub token: String,
665 pub mint_url: String,
668 pub buyer_pubkey: String,
670 pub seller_pubkey: String,
672 pub mostro_pubkey: String,
675 #[serde(default, skip_serializing_if = "Option::is_none")]
680 pub fee_token: Option<String>,
681}
682
683impl CashuLockProof {
684 pub fn new(
686 token: String,
687 mint_url: String,
688 buyer_pubkey: String,
689 seller_pubkey: String,
690 mostro_pubkey: String,
691 ) -> Self {
692 Self {
693 token,
694 mint_url,
695 buyer_pubkey,
696 seller_pubkey,
697 mostro_pubkey,
698 fee_token: None,
699 }
700 }
701
702 pub fn with_fee_token(mut self, fee_token: String) -> Self {
705 self.fee_token = Some(fee_token);
706 self
707 }
708
709 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
711 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
712 }
713
714 pub fn as_json(&self) -> Result<String, ServiceError> {
716 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
717 }
718}
719
720#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
729pub struct CashuProofSignature {
730 pub secret: String,
734 pub signature: String,
737}
738
739impl CashuProofSignature {
740 pub fn new(secret: String, signature: String) -> Self {
742 Self { secret, signature }
743 }
744}
745
746#[derive(Debug, Deserialize, Serialize, Clone)]
752#[serde(rename_all = "snake_case")]
753pub enum Payload {
754 Order(SmallOrder),
756 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
764 TextMessage(String),
766 Peer(Peer),
768 RatingUser(u8),
770 Amount(Amount),
772 Dispute(Uuid, Option<SolverDisputeInfo>),
775 CantDo(Option<CantDoReason>),
777 NextTrade(String, u32),
780 PaymentFailed(PaymentFailedInfo),
782 RestoreData(RestoreSessionInfo),
784 Ids(Vec<Uuid>),
786 Orders(Vec<SmallOrder>),
788 BondResolution(BondResolution),
791 BondPayoutRequest(BondPayoutRequest),
797 CashuLockProof(CashuLockProof),
800 CashuSignatures(Vec<CashuProofSignature>),
806 PayerDeclaration(PayerDeclaration),
810 PaymentHistory(PaymentHistory),
814 ReputationExportRequest(ReputationExportRequest),
817 ReputationAttestation(String),
823 UserInfo(UserInfo),
826}
827
828#[allow(dead_code)]
829impl MessageKind {
830 pub fn new(
833 id: Option<Uuid>,
834 request_id: Option<u64>,
835 trade_index: Option<i64>,
836 action: Action,
837 payload: Option<Payload>,
838 ) -> Self {
839 Self {
840 version: PROTOCOL_VER,
841 request_id,
842 trade_index,
843 id,
844 action,
845 payload,
846 }
847 }
848 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
850 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
851 }
852 pub fn as_json(&self) -> Result<String, ServiceError> {
854 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
855 }
856
857 pub fn get_action(&self) -> Action {
859 self.action.clone()
860 }
861
862 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
869 match &self.payload {
870 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
871 None => Ok(None),
872 _ => Err(ServiceError::InvalidPayload),
873 }
874 }
875
876 pub fn get_rating(&self) -> Result<u8, ServiceError> {
884 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
885 if !(MIN_RATING..=MAX_RATING).contains(&v) {
886 return Err(ServiceError::InvalidRatingValue);
887 }
888 Ok(v)
889 } else {
890 Err(ServiceError::InvalidRating)
891 }
892 }
893
894 pub fn verify(&self) -> bool {
901 match &self.action {
902 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
903 Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
904 if self.id.is_none() {
905 return false;
906 }
907 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
908 }
909 Action::AddBondInvoice => {
910 if self.id.is_none() {
911 return false;
912 }
913 matches!(
919 &self.payload,
920 Some(Payload::BondPayoutRequest(_)) | Some(Payload::PaymentRequest(_, _, _))
921 )
922 }
923 Action::AdminSettle | Action::AdminCancel => {
924 if self.id.is_none() {
925 return false;
926 }
927 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
928 }
929 Action::AddCashuEscrow => {
930 if self.id.is_none() {
931 return false;
932 }
933 matches!(&self.payload, Some(Payload::CashuLockProof(_)))
934 }
935 Action::CashuPmSignature => {
936 if self.id.is_none() {
937 return false;
938 }
939 matches!(&self.payload, Some(Payload::CashuSignatures(sigs)) if !sigs.is_empty())
940 }
941 Action::DeclarePayer | Action::PayerDeclared => {
942 if self.id.is_none() {
943 return false;
944 }
945 matches!(&self.payload, Some(Payload::PayerDeclaration(_)))
946 }
947 Action::PaymentHistory => {
948 if self.id.is_none() {
949 return false;
950 }
951 matches!(&self.payload, None | Some(Payload::PaymentHistory(_)))
954 }
955 Action::TakeSell
956 | Action::TakeBuy
957 | Action::FiatSent
958 | Action::FiatSentOk
959 | Action::Release
960 | Action::Released
961 | Action::Dispute
962 | Action::AdminCanceled
963 | Action::AdminSettled
964 | Action::Rate
965 | Action::RateReceived
966 | Action::AdminTakeDispute
967 | Action::AdminTookDispute
968 | Action::DisputeInitiatedByYou
969 | Action::DisputeInitiatedByPeer
970 | Action::WaitingBuyerInvoice
971 | Action::PurchaseCompleted
972 | Action::BondPayoutCompleted
973 | Action::BondSlashed
974 | Action::HoldInvoicePaymentAccepted
975 | Action::HoldInvoicePaymentSettled
976 | Action::HoldInvoicePaymentCanceled
977 | Action::WaitingSellerToPay
978 | Action::BuyerTookOrder
979 | Action::BuyerInvoiceAccepted
980 | Action::BondInvoiceAccepted
981 | Action::CooperativeCancelInitiatedByYou
982 | Action::CooperativeCancelInitiatedByPeer
983 | Action::CooperativeCancelAccepted
984 | Action::Cancel
985 | Action::InvoiceUpdated
986 | Action::AdminAddSolver
987 | Action::SendDm
988 | Action::TradePubkey
989 | Action::CashuEscrowLocked
990 | Action::Canceled => {
991 if self.id.is_none() {
992 return false;
993 }
994 !matches!(
996 &self.payload,
997 Some(Payload::BondResolution(_))
998 | Some(Payload::BondPayoutRequest(_))
999 | Some(Payload::PayerDeclaration(_))
1000 | Some(Payload::PaymentHistory(_))
1001 | Some(Payload::UserInfo(_))
1002 )
1003 }
1004 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
1005 Action::UserInfo => {
1006 self.id.is_none() && matches!(&self.payload, None | Some(Payload::UserInfo(_)))
1007 }
1008 Action::PaymentFailed => {
1009 if self.id.is_none() {
1010 return false;
1011 }
1012 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
1013 }
1014 Action::RateUser => {
1015 matches!(&self.payload, Some(Payload::RatingUser(_)))
1016 }
1017 Action::CantDo => {
1018 matches!(&self.payload, Some(Payload::CantDo(_)))
1019 }
1020 Action::Orders => {
1021 matches!(
1022 &self.payload,
1023 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
1024 )
1025 }
1026 Action::ExportReputation => {
1027 self.id.is_none()
1028 && matches!(&self.payload, Some(Payload::ReputationExportRequest(_)))
1029 }
1030 Action::ReputationExported | Action::ImportReputation => {
1031 self.id.is_none()
1032 && matches!(&self.payload, Some(Payload::ReputationAttestation(_)))
1033 }
1034 Action::ReputationImported => self.id.is_none() && self.payload.is_none(),
1035 }
1036 }
1037
1038 pub fn get_order(&self) -> Option<&SmallOrder> {
1043 if self.action != Action::NewOrder {
1044 return None;
1045 }
1046 match &self.payload {
1047 Some(Payload::Order(o)) => Some(o),
1048 _ => None,
1049 }
1050 }
1051
1052 pub fn get_payment_request(&self) -> Option<String> {
1059 if self.action != Action::TakeSell
1060 && self.action != Action::AddInvoice
1061 && self.action != Action::AddBondInvoice
1062 && self.action != Action::NewOrder
1063 {
1064 return None;
1065 }
1066 match &self.payload {
1067 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
1068 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
1069 _ => None,
1070 }
1071 }
1072
1073 pub fn get_amount(&self) -> Option<Amount> {
1077 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
1078 return None;
1079 }
1080 match &self.payload {
1081 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
1082 Some(Payload::Amount(amount)) => Some(*amount),
1083 _ => None,
1084 }
1085 }
1086
1087 pub fn get_payload(&self) -> Option<&Payload> {
1089 self.payload.as_ref()
1090 }
1091
1092 pub fn has_trade_index(&self) -> (bool, i64) {
1095 if let Some(index) = self.trade_index {
1096 return (true, index);
1097 }
1098 (false, 0)
1099 }
1100
1101 pub fn trade_index(&self) -> i64 {
1103 if let Some(index) = self.trade_index {
1104 return index;
1105 }
1106 0
1107 }
1108}
1109
1110#[cfg(test)]
1111mod test {
1112 use crate::message::{
1113 Action, BondPayoutRequest, CashuLockProof, CashuProofSignature, Message, MessageKind,
1114 Payload, Peer,
1115 };
1116 use crate::order::SmallOrder;
1117 use crate::payer::{payment_hash, BuyerMode, PayerDeclaration, PaymentHistory};
1118 use crate::user::UserInfo;
1119 use nostr_sdk::prelude::Keys;
1120 use uuid::uuid;
1121
1122 #[test]
1123 fn test_peer_with_reputation() {
1124 let reputation = UserInfo {
1126 rating: 4.5,
1127 reviews: 10,
1128 operating_days: 30,
1129 since: None,
1130 };
1131 let peer = Peer::new(
1132 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1133 Some(reputation.clone()),
1134 );
1135
1136 assert_eq!(
1138 peer.pubkey,
1139 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1140 );
1141 assert!(peer.reputation.is_some());
1142 let peer_reputation = peer.reputation.clone().unwrap();
1143 assert_eq!(peer_reputation.rating, 4.5);
1144 assert_eq!(peer_reputation.reviews, 10);
1145 assert_eq!(peer_reputation.operating_days, 30);
1146
1147 let json = peer.as_json().unwrap();
1149 let deserialized_peer = Peer::from_json(&json).unwrap();
1150 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1151 assert!(deserialized_peer.reputation.is_some());
1152 let deserialized_reputation = deserialized_peer.reputation.unwrap();
1153 assert_eq!(deserialized_reputation.rating, 4.5);
1154 assert_eq!(deserialized_reputation.reviews, 10);
1155 assert_eq!(deserialized_reputation.operating_days, 30);
1156 }
1157
1158 #[test]
1159 fn test_peer_without_reputation() {
1160 let peer = Peer::new(
1162 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1163 None,
1164 );
1165
1166 assert_eq!(
1168 peer.pubkey,
1169 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1170 );
1171 assert!(peer.reputation.is_none());
1172
1173 let json = peer.as_json().unwrap();
1175 let deserialized_peer = Peer::from_json(&json).unwrap();
1176 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1177 assert!(deserialized_peer.reputation.is_none());
1178 }
1179
1180 #[test]
1181 fn test_peer_in_message() {
1182 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1183
1184 let reputation = UserInfo {
1186 rating: 4.5,
1187 reviews: 10,
1188 operating_days: 30,
1189 since: None,
1190 };
1191 let peer_with_reputation = Peer::new(
1192 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1193 Some(reputation),
1194 );
1195 let payload_with_reputation = Payload::Peer(peer_with_reputation);
1196 let message_with_reputation = Message::Order(MessageKind::new(
1197 Some(uuid),
1198 Some(1),
1199 Some(2),
1200 Action::FiatSentOk,
1201 Some(payload_with_reputation),
1202 ));
1203
1204 assert!(message_with_reputation.verify());
1206 let message_json = message_with_reputation.as_json().unwrap();
1207 let deserialized_message = Message::from_json(&message_json).unwrap();
1208 assert!(deserialized_message.verify());
1209
1210 let peer_without_reputation = Peer::new(
1212 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1213 None,
1214 );
1215 let payload_without_reputation = Payload::Peer(peer_without_reputation);
1216 let message_without_reputation = Message::Order(MessageKind::new(
1217 Some(uuid),
1218 Some(1),
1219 Some(2),
1220 Action::FiatSentOk,
1221 Some(payload_without_reputation),
1222 ));
1223
1224 assert!(message_without_reputation.verify());
1226 let message_json = message_without_reputation.as_json().unwrap();
1227 let deserialized_message = Message::from_json(&message_json).unwrap();
1228 assert!(deserialized_message.verify());
1229 }
1230
1231 #[test]
1232 fn test_bond_payout_request_payload_verifies_on_add_bond_invoice() {
1233 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1234 let order = SmallOrder {
1235 id: Some(order_id),
1236 kind: None,
1237 status: None,
1238 amount: 500,
1239 fiat_code: "USD".to_string(),
1240 min_amount: None,
1241 max_amount: None,
1242 fiat_amount: 0,
1243 payment_method: "lightning".to_string(),
1244 premium: 0,
1245 buyer_trade_pubkey: None,
1246 seller_trade_pubkey: None,
1247 buyer_invoice: None,
1248 created_at: None,
1249 expires_at: None,
1250 cashu_mint_url: None,
1251 };
1252 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1253 order,
1254 slashed_at: 1_734_000_000,
1255 });
1256 let kind = MessageKind::new(
1257 Some(order_id),
1258 None,
1259 None,
1260 Action::AddBondInvoice,
1261 Some(payload),
1262 );
1263 assert!(
1264 kind.verify(),
1265 "BondPayoutRequest must verify on AddBondInvoice"
1266 );
1267
1268 let m = Message::Order(kind);
1271 let json = m.as_json().unwrap();
1272 assert!(json.contains("bond_payout_request"));
1273 let back = Message::from_json(&json).unwrap();
1274 assert!(back.verify());
1275 }
1276
1277 #[test]
1278 fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1279 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1282 let order = SmallOrder {
1283 id: Some(order_id),
1284 kind: None,
1285 status: None,
1286 amount: 500,
1287 fiat_code: "USD".to_string(),
1288 min_amount: None,
1289 max_amount: None,
1290 fiat_amount: 0,
1291 payment_method: "lightning".to_string(),
1292 premium: 0,
1293 buyer_trade_pubkey: None,
1294 seller_trade_pubkey: None,
1295 buyer_invoice: None,
1296 created_at: None,
1297 expires_at: None,
1298 cashu_mint_url: None,
1299 };
1300
1301 let _exhaustive: fn(Action) = |a| match a {
1306 Action::AddBondInvoice => {}
1307 Action::NewOrder
1308 | Action::TakeSell
1309 | Action::TakeBuy
1310 | Action::PayInvoice
1311 | Action::PayBondInvoice
1312 | Action::FiatSent
1313 | Action::FiatSentOk
1314 | Action::Release
1315 | Action::Released
1316 | Action::Cancel
1317 | Action::Canceled
1318 | Action::CooperativeCancelInitiatedByYou
1319 | Action::CooperativeCancelInitiatedByPeer
1320 | Action::DisputeInitiatedByYou
1321 | Action::DisputeInitiatedByPeer
1322 | Action::CooperativeCancelAccepted
1323 | Action::BuyerInvoiceAccepted
1324 | Action::BondInvoiceAccepted
1325 | Action::PurchaseCompleted
1326 | Action::BondPayoutCompleted
1327 | Action::BondSlashed
1328 | Action::HoldInvoicePaymentAccepted
1329 | Action::HoldInvoicePaymentSettled
1330 | Action::HoldInvoicePaymentCanceled
1331 | Action::WaitingSellerToPay
1332 | Action::WaitingBuyerInvoice
1333 | Action::AddInvoice
1334 | Action::BuyerTookOrder
1335 | Action::Rate
1336 | Action::RateUser
1337 | Action::RateReceived
1338 | Action::CantDo
1339 | Action::Dispute
1340 | Action::AdminCancel
1341 | Action::AdminCanceled
1342 | Action::AdminSettle
1343 | Action::AdminSettled
1344 | Action::AdminAddSolver
1345 | Action::AdminTakeDispute
1346 | Action::AdminTookDispute
1347 | Action::PaymentFailed
1348 | Action::InvoiceUpdated
1349 | Action::SendDm
1350 | Action::TradePubkey
1351 | Action::RestoreSession
1352 | Action::LastTradeIndex
1353 | Action::UserInfo
1354 | Action::AddCashuEscrow
1355 | Action::CashuEscrowLocked
1356 | Action::CashuPmSignature
1357 | Action::DeclarePayer
1358 | Action::PayerDeclared
1359 | Action::PaymentHistory
1360 | Action::ExportReputation
1361 | Action::ReputationExported
1362 | Action::ImportReputation
1363 | Action::ReputationImported
1364 | Action::Orders => {}
1365 };
1366
1367 let other_actions: &[Action] = &[
1368 Action::NewOrder,
1369 Action::TakeSell,
1370 Action::TakeBuy,
1371 Action::PayInvoice,
1372 Action::PayBondInvoice,
1373 Action::FiatSent,
1374 Action::FiatSentOk,
1375 Action::Release,
1376 Action::Released,
1377 Action::Cancel,
1378 Action::Canceled,
1379 Action::CooperativeCancelInitiatedByYou,
1380 Action::CooperativeCancelInitiatedByPeer,
1381 Action::DisputeInitiatedByYou,
1382 Action::DisputeInitiatedByPeer,
1383 Action::CooperativeCancelAccepted,
1384 Action::BuyerInvoiceAccepted,
1385 Action::BondInvoiceAccepted,
1386 Action::PurchaseCompleted,
1387 Action::BondPayoutCompleted,
1388 Action::BondSlashed,
1389 Action::HoldInvoicePaymentAccepted,
1390 Action::HoldInvoicePaymentSettled,
1391 Action::HoldInvoicePaymentCanceled,
1392 Action::WaitingSellerToPay,
1393 Action::WaitingBuyerInvoice,
1394 Action::AddInvoice,
1395 Action::BuyerTookOrder,
1396 Action::Rate,
1397 Action::RateUser,
1398 Action::RateReceived,
1399 Action::CantDo,
1400 Action::Dispute,
1401 Action::AdminCancel,
1402 Action::AdminCanceled,
1403 Action::AdminSettle,
1404 Action::AdminSettled,
1405 Action::AdminAddSolver,
1406 Action::AdminTakeDispute,
1407 Action::AdminTookDispute,
1408 Action::PaymentFailed,
1409 Action::InvoiceUpdated,
1410 Action::SendDm,
1411 Action::TradePubkey,
1412 Action::RestoreSession,
1413 Action::LastTradeIndex,
1414 Action::UserInfo,
1415 Action::Orders,
1416 Action::AddCashuEscrow,
1417 Action::CashuEscrowLocked,
1418 Action::CashuPmSignature,
1419 Action::DeclarePayer,
1420 Action::PayerDeclared,
1421 Action::PaymentHistory,
1422 Action::ExportReputation,
1423 Action::ReputationExported,
1424 Action::ImportReputation,
1425 Action::ReputationImported,
1426 ];
1427
1428 for action in other_actions {
1429 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1430 order: order.clone(),
1431 slashed_at: 0,
1432 });
1433 let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1434 assert!(
1435 !kind.verify(),
1436 "BondPayoutRequest must be rejected on {action:?}"
1437 );
1438 }
1439 }
1440
1441 #[test]
1442 fn test_payer_payloads_rejected_on_unrelated_actions() {
1443 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1446 let unrelated = [
1447 Action::NewOrder,
1448 Action::TakeSell,
1449 Action::TakeBuy,
1450 Action::PayInvoice,
1451 Action::PayBondInvoice,
1452 Action::FiatSent,
1453 Action::FiatSentOk,
1454 Action::Release,
1455 Action::Released,
1456 Action::Cancel,
1457 Action::Canceled,
1458 Action::CooperativeCancelInitiatedByYou,
1459 Action::CooperativeCancelInitiatedByPeer,
1460 Action::DisputeInitiatedByYou,
1461 Action::DisputeInitiatedByPeer,
1462 Action::CooperativeCancelAccepted,
1463 Action::BuyerInvoiceAccepted,
1464 Action::BondInvoiceAccepted,
1465 Action::PurchaseCompleted,
1466 Action::BondPayoutCompleted,
1467 Action::BondSlashed,
1468 Action::HoldInvoicePaymentAccepted,
1469 Action::HoldInvoicePaymentSettled,
1470 Action::HoldInvoicePaymentCanceled,
1471 Action::WaitingSellerToPay,
1472 Action::WaitingBuyerInvoice,
1473 Action::AddInvoice,
1474 Action::BuyerTookOrder,
1475 Action::Rate,
1476 Action::RateUser,
1477 Action::RateReceived,
1478 Action::CantDo,
1479 Action::Dispute,
1480 Action::AdminCancel,
1481 Action::AdminCanceled,
1482 Action::AdminSettle,
1483 Action::AdminSettled,
1484 Action::AdminAddSolver,
1485 Action::AdminTakeDispute,
1486 Action::AdminTookDispute,
1487 Action::PaymentFailed,
1488 Action::InvoiceUpdated,
1489 Action::SendDm,
1490 Action::TradePubkey,
1491 Action::RestoreSession,
1492 Action::LastTradeIndex,
1493 Action::UserInfo,
1494 Action::Orders,
1495 Action::AddCashuEscrow,
1496 Action::CashuEscrowLocked,
1497 Action::CashuPmSignature,
1498 Action::AddBondInvoice,
1499 ];
1500 for action in unrelated {
1501 for payload in [
1502 Payload::PayerDeclaration(PayerDeclaration::new("a".repeat(64))),
1503 Payload::PaymentHistory(PaymentHistory::unavailable("a".repeat(64))),
1504 ] {
1505 let kind =
1506 MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1507 assert!(!kind.verify(), "{action:?} must reject {:?}", kind.payload);
1508 }
1509 }
1510 }
1511
1512 #[test]
1513 fn test_bond_payout_ack_actions_verify_and_wire_format() {
1514 use crate::message::BondResolution;
1515
1516 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1517
1518 let order = || SmallOrder {
1521 id: Some(order_id),
1522 kind: None,
1523 status: None,
1524 amount: 500,
1525 fiat_code: "USD".to_string(),
1526 min_amount: None,
1527 max_amount: None,
1528 fiat_amount: 0,
1529 payment_method: "lightning".to_string(),
1530 premium: 0,
1531 buyer_trade_pubkey: None,
1532 seller_trade_pubkey: None,
1533 buyer_invoice: None,
1534 created_at: None,
1535 expires_at: None,
1536 cashu_mint_url: None,
1537 };
1538
1539 for (action, discriminator) in [
1545 (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1546 (Action::BondPayoutCompleted, "bond-payout-completed"),
1547 (Action::BondSlashed, "bond-slashed"),
1548 ] {
1549 let ok = Message::Order(MessageKind::new(
1551 Some(order_id),
1552 Some(1),
1553 Some(2),
1554 action.clone(),
1555 Some(Payload::Order(order())),
1556 ));
1557 assert!(ok.verify(), "{action:?} + Order should verify");
1558
1559 let no_id = Message::Order(MessageKind::new(
1561 None,
1562 Some(1),
1563 Some(2),
1564 action.clone(),
1565 Some(Payload::Order(order())),
1566 ));
1567 assert!(!no_id.verify(), "{action:?} without id must be rejected");
1568
1569 let with_resolution = Message::Order(MessageKind::new(
1571 Some(order_id),
1572 Some(1),
1573 Some(2),
1574 action.clone(),
1575 Some(Payload::BondResolution(BondResolution {
1576 slash_seller: true,
1577 slash_buyer: false,
1578 })),
1579 ));
1580 assert!(
1581 !with_resolution.verify(),
1582 "{action:?} + BondResolution must be rejected"
1583 );
1584
1585 let with_request = Message::Order(MessageKind::new(
1587 Some(order_id),
1588 Some(1),
1589 Some(2),
1590 action.clone(),
1591 Some(Payload::BondPayoutRequest(BondPayoutRequest {
1592 order: order(),
1593 slashed_at: 0,
1594 })),
1595 ));
1596 assert!(
1597 !with_request.verify(),
1598 "{action:?} + BondPayoutRequest must be rejected"
1599 );
1600
1601 let json = ok.as_json().unwrap();
1603 assert!(
1604 json.contains(&format!("\"action\":\"{discriminator}\"")),
1605 "expected kebab-case discriminator {discriminator}, got: {json}"
1606 );
1607 let decoded = Message::from_json(&json).unwrap();
1608 assert!(decoded.verify());
1609 assert_eq!(decoded.inner_action(), Some(action));
1610 }
1611 }
1612
1613 #[test]
1614 fn test_payment_failed_payload() {
1615 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1616
1617 let payment_failed_info = crate::message::PaymentFailedInfo {
1619 payment_attempts: 3,
1620 payment_retries_interval: 60,
1621 };
1622
1623 let payload = Payload::PaymentFailed(payment_failed_info);
1624 let message = Message::Order(MessageKind::new(
1625 Some(uuid),
1626 Some(1),
1627 Some(2),
1628 Action::PaymentFailed,
1629 Some(payload),
1630 ));
1631
1632 assert!(message.verify());
1634
1635 let message_json = message.as_json().unwrap();
1637
1638 let deserialized_message = Message::from_json(&message_json).unwrap();
1640 assert!(deserialized_message.verify());
1641
1642 if let Message::Order(kind) = deserialized_message {
1644 if let Some(Payload::PaymentFailed(info)) = kind.payload {
1645 assert_eq!(info.payment_attempts, 3);
1646 assert_eq!(info.payment_retries_interval, 60);
1647 } else {
1648 panic!("Expected PaymentFailed payload");
1649 }
1650 } else {
1651 panic!("Expected Order message");
1652 }
1653 }
1654
1655 #[test]
1656 fn test_message_payload_signature() {
1657 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1658 let peer = Peer::new(
1659 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1660 None, );
1662 let payload = Payload::Peer(peer);
1663 let test_message = Message::Order(MessageKind::new(
1664 Some(uuid),
1665 Some(1),
1666 Some(2),
1667 Action::FiatSentOk,
1668 Some(payload),
1669 ));
1670 assert!(test_message.verify());
1671 let test_message_json = test_message.as_json().unwrap();
1672 let trade_keys =
1674 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1675 .unwrap();
1676 let sig = Message::sign(test_message_json.clone(), &trade_keys);
1677
1678 assert!(Message::verify_signature(
1679 test_message_json,
1680 trade_keys.public_key(),
1681 sig
1682 ));
1683 }
1684
1685 #[test]
1686 fn test_restore_session_message() {
1687 let restore_request_message = Message::Restore(MessageKind::new(
1689 None,
1690 None,
1691 None,
1692 Action::RestoreSession,
1693 None,
1694 ));
1695
1696 assert!(restore_request_message.verify());
1698 assert_eq!(
1699 restore_request_message.inner_action(),
1700 Some(Action::RestoreSession)
1701 );
1702
1703 let message_json = restore_request_message.as_json().unwrap();
1705 let deserialized_message = Message::from_json(&message_json).unwrap();
1706 assert!(deserialized_message.verify());
1707 assert_eq!(
1708 deserialized_message.inner_action(),
1709 Some(Action::RestoreSession)
1710 );
1711
1712 let restored_orders = vec![
1714 crate::message::RestoredOrdersInfo {
1715 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1716 trade_index: 1,
1717 status: "active".to_string(),
1718 counterparty_trade_pubkey: Some(
1719 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1720 ),
1721 },
1722 crate::message::RestoredOrdersInfo {
1723 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1724 trade_index: 2,
1725 status: "success".to_string(),
1726 counterparty_trade_pubkey: None,
1727 },
1728 ];
1729
1730 let restored_disputes = vec![
1731 crate::message::RestoredDisputesInfo {
1732 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1733 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1734 trade_index: 1,
1735 status: "initiated".to_string(),
1736 initiator: Some(crate::message::DisputeInitiator::Buyer),
1737 solver_pubkey: None,
1738 },
1739 crate::message::RestoredDisputesInfo {
1740 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1741 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1742 trade_index: 2,
1743 status: "in-progress".to_string(),
1744 initiator: None,
1745 solver_pubkey: Some(
1746 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1747 ),
1748 },
1749 crate::message::RestoredDisputesInfo {
1750 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1751 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1752 trade_index: 3,
1753 status: "initiated".to_string(),
1754 initiator: Some(crate::message::DisputeInitiator::Seller),
1755 solver_pubkey: None,
1756 },
1757 ];
1758
1759 let restore_session_info = crate::message::RestoreSessionInfo {
1760 restore_orders: restored_orders.clone(),
1761 restore_disputes: restored_disputes.clone(),
1762 };
1763
1764 let restore_data_payload = Payload::RestoreData(restore_session_info);
1765 let restore_data_message = Message::Restore(MessageKind::new(
1766 None,
1767 None,
1768 None,
1769 Action::RestoreSession,
1770 Some(restore_data_payload),
1771 ));
1772
1773 assert!(!restore_data_message.verify());
1775
1776 let message_json = restore_data_message.as_json().unwrap();
1778 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1779
1780 if let Message::Restore(kind) = deserialized_restore_message {
1781 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1782 assert_eq!(session_info.restore_disputes.len(), 3);
1783 assert_eq!(
1784 session_info.restore_disputes[0].initiator,
1785 Some(crate::message::DisputeInitiator::Buyer)
1786 );
1787 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1788 assert_eq!(session_info.restore_disputes[1].initiator, None);
1789 assert_eq!(
1790 session_info.restore_disputes[1].solver_pubkey,
1791 Some(
1792 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1793 .to_string()
1794 )
1795 );
1796 assert_eq!(
1797 session_info.restore_disputes[2].initiator,
1798 Some(crate::message::DisputeInitiator::Seller)
1799 );
1800 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1801 assert_eq!(
1802 session_info.restore_orders[0].counterparty_trade_pubkey,
1803 Some(
1804 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1805 .to_string()
1806 )
1807 );
1808 assert!(session_info.restore_orders[1]
1809 .counterparty_trade_pubkey
1810 .is_none());
1811 } else {
1812 panic!("Expected RestoreData payload");
1813 }
1814 } else {
1815 panic!("Expected Restore message");
1816 }
1817 }
1818
1819 #[test]
1822 fn restored_order_without_counterparty_pubkey_still_parses() {
1823 let json = r#"{
1824 "order_id": "308e1272-d5f4-47e6-bd97-3504baea9c23",
1825 "trade_index": 1,
1826 "status": "active"
1827 }"#;
1828
1829 let order: crate::message::RestoredOrdersInfo = serde_json::from_str(json).unwrap();
1830
1831 assert_eq!(order.trade_index, 1);
1832 assert_eq!(order.status, "active");
1833 assert!(order.counterparty_trade_pubkey.is_none());
1834 }
1835
1836 #[test]
1837 fn test_restore_session_message_validation() {
1838 let restore_request_message = Message::Restore(MessageKind::new(
1840 None,
1841 None,
1842 None,
1843 Action::RestoreSession,
1844 None, ));
1846
1847 assert!(restore_request_message.verify());
1849
1850 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1852 let wrong_message = Message::Restore(MessageKind::new(
1853 None,
1854 None,
1855 None,
1856 Action::RestoreSession,
1857 Some(wrong_payload),
1858 ));
1859
1860 assert!(!wrong_message.verify());
1862
1863 let with_id = Message::Restore(MessageKind::new(
1865 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1866 None,
1867 None,
1868 Action::RestoreSession,
1869 None,
1870 ));
1871 assert!(with_id.verify());
1872
1873 let with_request_id = Message::Restore(MessageKind::new(
1874 None,
1875 Some(42),
1876 None,
1877 Action::RestoreSession,
1878 None,
1879 ));
1880 assert!(with_request_id.verify());
1881
1882 let with_trade_index = Message::Restore(MessageKind::new(
1883 None,
1884 None,
1885 Some(7),
1886 Action::RestoreSession,
1887 None,
1888 ));
1889 assert!(with_trade_index.verify());
1890 }
1891
1892 #[test]
1893 fn test_restore_session_message_constructor() {
1894 let restore_request_message = Message::new_restore(None);
1896
1897 assert!(matches!(restore_request_message, Message::Restore(_)));
1898 assert!(restore_request_message.verify());
1899 assert_eq!(
1900 restore_request_message.inner_action(),
1901 Some(Action::RestoreSession)
1902 );
1903
1904 let restore_session_info = crate::message::RestoreSessionInfo {
1906 restore_orders: vec![],
1907 restore_disputes: vec![],
1908 };
1909 let restore_data_message =
1910 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1911
1912 assert!(matches!(restore_data_message, Message::Restore(_)));
1913 assert!(!restore_data_message.verify());
1914 }
1915
1916 #[test]
1917 fn test_last_trade_index_valid_message() {
1918 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1919 let msg = Message::Restore(kind);
1920
1921 assert!(msg.verify());
1922
1923 let json = msg.as_json().unwrap();
1925 let decoded = Message::from_json(&json).unwrap();
1926 assert!(decoded.verify());
1927
1928 let inner = decoded.get_inner_message_kind();
1930 assert_eq!(inner.trade_index(), 7);
1931 assert_eq!(inner.has_trade_index(), (true, 7));
1932 }
1933
1934 #[test]
1935 fn test_last_trade_index_without_id_is_valid() {
1936 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1938 let msg = Message::Restore(kind);
1939 assert!(msg.verify());
1940 }
1941
1942 #[test]
1943 fn test_last_trade_index_with_payload_fails_validation() {
1944 let kind = MessageKind::new(
1946 None,
1947 None,
1948 Some(3),
1949 Action::LastTradeIndex,
1950 Some(Payload::TextMessage("ignored".to_string())),
1951 );
1952 let msg = Message::Restore(kind);
1953 assert!(!msg.verify());
1954 }
1955
1956 #[test]
1957 fn test_user_info_request_wire_format() {
1958 let json =
1959 r#"{"restore":{"version":2,"request_id":123456,"action":"user-info","payload":null}}"#;
1960 let msg = Message::from_json(json).unwrap();
1961 assert!(msg.verify());
1962 let inner = msg.get_inner_message_kind();
1963 assert_eq!(inner.action, Action::UserInfo);
1964 assert_eq!(inner.request_id, Some(123456));
1965 assert!(inner.id.is_none());
1966 assert!(inner.payload.is_none());
1967
1968 let kind = MessageKind::new(None, Some(123456), None, Action::UserInfo, None);
1969 let out = Message::Restore(kind).as_json().unwrap();
1970 assert!(out.contains(r#""action":"user-info""#));
1971 }
1972
1973 #[test]
1974 fn test_user_info_response_wire_format() {
1975 let json = r#"{"restore":{"version":2,"request_id":123456,"action":"user-info","payload":{"user_info":{"rating":4.8,"reviews":23,"operating_days":142,"since":1700784000}}}}"#;
1976 let msg = Message::from_json(json).unwrap();
1977 assert!(msg.verify());
1978 match msg.get_inner_message_kind().payload.as_ref() {
1979 Some(Payload::UserInfo(info)) => {
1980 assert_eq!(info.rating, 4.8);
1981 assert_eq!(info.reviews, 23);
1982 assert_eq!(info.operating_days, 142);
1983 assert_eq!(info.since, Some(1_700_784_000));
1984 }
1985 other => panic!("unexpected payload: {other:?}"),
1986 }
1987 let value: serde_json::Value = serde_json::from_str(&msg.as_json().unwrap()).unwrap();
1988 let expected: serde_json::Value = serde_json::from_str(json).unwrap();
1989 assert_eq!(value["restore"]["action"], expected["restore"]["action"]);
1990 assert_eq!(value["restore"]["payload"], expected["restore"]["payload"]);
1991 }
1992
1993 #[test]
1994 fn test_user_info_response_unknown_identity_omits_since() {
1995 let info = UserInfo {
1996 rating: 0.0,
1997 reviews: 0,
1998 operating_days: 0,
1999 since: None,
2000 };
2001 let kind = MessageKind::new(
2002 None,
2003 None,
2004 None,
2005 Action::UserInfo,
2006 Some(Payload::UserInfo(info)),
2007 );
2008 let msg = Message::Restore(kind);
2009 assert!(msg.verify());
2010 let json = msg.as_json().unwrap();
2011 assert!(json.contains(r#""payload":{"user_info":{"#));
2012 assert!(!json.contains("since"));
2013 }
2014
2015 #[test]
2016 fn test_user_info_rejects_wrong_payload_and_id() {
2017 let wrong_payload = MessageKind::new(
2018 None,
2019 None,
2020 None,
2021 Action::UserInfo,
2022 Some(Payload::Peer(Peer::new("pubkey".to_string(), None))),
2023 );
2024 assert!(!Message::Restore(wrong_payload).verify());
2025
2026 let with_id = MessageKind::new(
2027 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
2028 None,
2029 None,
2030 Action::UserInfo,
2031 None,
2032 );
2033 assert!(!Message::Restore(with_id).verify());
2034
2035 let on_order_action = MessageKind::new(
2036 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
2037 None,
2038 None,
2039 Action::FiatSent,
2040 Some(Payload::UserInfo(UserInfo {
2041 rating: 0.0,
2042 reviews: 0,
2043 operating_days: 0,
2044 since: None,
2045 })),
2046 );
2047 assert!(!on_order_action.verify());
2048 }
2049
2050 #[test]
2051 fn test_user_info_rejected_outside_restore_wrapper() {
2052 let request = MessageKind::new(None, Some(1), None, Action::UserInfo, None);
2053 assert!(Message::Restore(request.clone()).verify());
2054 for msg in [
2055 Message::Order(request.clone()),
2056 Message::Dispute(request.clone()),
2057 Message::CantDo(request.clone()),
2058 Message::Rate(request.clone()),
2059 Message::Dm(request),
2060 ] {
2061 assert!(!msg.verify(), "user-info must be rejected in {msg:?}");
2062 }
2063 }
2064
2065 #[test]
2066 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
2067 use crate::message::BondResolution;
2068
2069 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2070
2071 for action in [Action::AdminSettle, Action::AdminCancel] {
2072 let with_resolution = Message::Order(MessageKind::new(
2073 Some(uuid),
2074 Some(1),
2075 Some(2),
2076 action.clone(),
2077 Some(Payload::BondResolution(BondResolution {
2078 slash_seller: true,
2079 slash_buyer: false,
2080 })),
2081 ));
2082 assert!(
2083 with_resolution.verify(),
2084 "{action:?} + BondResolution should verify"
2085 );
2086
2087 let without_payload = Message::Order(MessageKind::new(
2088 Some(uuid),
2089 Some(1),
2090 Some(2),
2091 action.clone(),
2092 None,
2093 ));
2094 assert!(without_payload.verify(), "{action:?} + None should verify");
2095
2096 let wrong = Message::Order(MessageKind::new(
2098 Some(uuid),
2099 Some(1),
2100 Some(2),
2101 action.clone(),
2102 Some(Payload::TextMessage("nope".to_string())),
2103 ));
2104 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
2105
2106 let no_id = Message::Order(MessageKind::new(
2108 None,
2109 Some(1),
2110 Some(2),
2111 action,
2112 Some(Payload::BondResolution(BondResolution {
2113 slash_seller: false,
2114 slash_buyer: false,
2115 })),
2116 ));
2117 assert!(!no_id.verify(), "admin action without id must be rejected");
2118 }
2119 }
2120
2121 #[test]
2122 fn test_bond_resolution_rejected_on_non_admin_actions() {
2123 use crate::message::BondResolution;
2124
2125 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2126 let payload = Payload::BondResolution(BondResolution {
2127 slash_seller: true,
2128 slash_buyer: true,
2129 });
2130
2131 for action in [
2135 Action::NewOrder,
2136 Action::TakeSell,
2137 Action::TakeBuy,
2138 Action::PayInvoice,
2139 Action::PayBondInvoice,
2140 Action::FiatSent,
2141 Action::FiatSentOk,
2142 Action::Release,
2143 Action::Released,
2144 Action::Cancel,
2145 Action::Canceled,
2146 Action::CooperativeCancelInitiatedByYou,
2147 Action::CooperativeCancelInitiatedByPeer,
2148 Action::DisputeInitiatedByYou,
2149 Action::DisputeInitiatedByPeer,
2150 Action::CooperativeCancelAccepted,
2151 Action::BuyerInvoiceAccepted,
2152 Action::BondInvoiceAccepted,
2153 Action::PurchaseCompleted,
2154 Action::BondPayoutCompleted,
2155 Action::BondSlashed,
2156 Action::HoldInvoicePaymentAccepted,
2157 Action::HoldInvoicePaymentSettled,
2158 Action::HoldInvoicePaymentCanceled,
2159 Action::WaitingSellerToPay,
2160 Action::WaitingBuyerInvoice,
2161 Action::AddInvoice,
2162 Action::AddBondInvoice,
2163 Action::BuyerTookOrder,
2164 Action::Rate,
2165 Action::RateUser,
2166 Action::RateReceived,
2167 Action::CantDo,
2168 Action::Dispute,
2169 Action::AdminCanceled,
2170 Action::AdminSettled,
2171 Action::AdminAddSolver,
2172 Action::AdminTakeDispute,
2173 Action::AdminTookDispute,
2174 Action::PaymentFailed,
2175 Action::InvoiceUpdated,
2176 Action::SendDm,
2177 Action::TradePubkey,
2178 Action::RestoreSession,
2179 Action::LastTradeIndex,
2180 Action::UserInfo,
2181 Action::Orders,
2182 ] {
2183 let msg = Message::Order(MessageKind::new(
2184 Some(uuid),
2185 Some(1),
2186 Some(2),
2187 action.clone(),
2188 Some(payload.clone()),
2189 ));
2190 assert!(
2191 !msg.verify(),
2192 "{action:?} must reject BondResolution payload"
2193 );
2194 }
2195 }
2196
2197 #[test]
2198 fn test_bond_resolution_wire_format() {
2199 use crate::message::BondResolution;
2200
2201 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2202 let msg = Message::Order(MessageKind::new(
2203 Some(uuid),
2204 None,
2205 None,
2206 Action::AdminCancel,
2207 Some(Payload::BondResolution(BondResolution {
2208 slash_seller: true,
2209 slash_buyer: false,
2210 })),
2211 ));
2212
2213 let json = msg.as_json().unwrap();
2214 assert!(
2216 json.contains("\"bond_resolution\""),
2217 "expected snake_case discriminator, got: {json}"
2218 );
2219 assert!(json.contains("\"slash_seller\":true"));
2220 assert!(json.contains("\"slash_buyer\":false"));
2221
2222 let decoded = Message::from_json(&json).unwrap();
2224 assert!(decoded.verify());
2225 if let Message::Order(kind) = decoded {
2226 match kind.payload {
2227 Some(Payload::BondResolution(b)) => {
2228 assert!(b.slash_seller);
2229 assert!(!b.slash_buyer);
2230 }
2231 other => panic!("expected BondResolution payload, got {other:?}"),
2232 }
2233 } else {
2234 panic!("expected Order message");
2235 }
2236 }
2237
2238 #[test]
2239 fn test_bond_resolution_legacy_null_payload() {
2240 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2244 let json = format!(
2245 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
2246 );
2247 let msg = Message::from_json(&json).unwrap();
2248 assert!(msg.verify());
2249 }
2250
2251 #[test]
2252 fn test_pay_bond_invoice_wire_format_and_verify() {
2253 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2254 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
2255
2256 let msg = Message::Order(MessageKind::new(
2257 Some(uuid),
2258 Some(1),
2259 Some(2),
2260 Action::PayBondInvoice,
2261 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2262 ));
2263 assert!(msg.verify());
2264
2265 let json = msg.as_json().unwrap();
2267 assert!(
2268 json.contains("\"action\":\"pay-bond-invoice\""),
2269 "expected kebab-case discriminator, got: {json}"
2270 );
2271
2272 let decoded = Message::from_json(&json).unwrap();
2274 assert!(decoded.verify());
2275 assert!(matches!(
2276 decoded.inner_action(),
2277 Some(Action::PayBondInvoice)
2278 ));
2279
2280 let no_id = Message::Order(MessageKind::new(
2282 None,
2283 Some(1),
2284 Some(2),
2285 Action::PayBondInvoice,
2286 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2287 ));
2288 assert!(!no_id.verify());
2289
2290 let wrong_payload = Message::Order(MessageKind::new(
2292 Some(uuid),
2293 Some(1),
2294 Some(2),
2295 Action::PayBondInvoice,
2296 Some(Payload::TextMessage("nope".to_string())),
2297 ));
2298 assert!(!wrong_payload.verify());
2299
2300 let no_payload = Message::Order(MessageKind::new(
2302 Some(uuid),
2303 Some(1),
2304 Some(2),
2305 Action::PayBondInvoice,
2306 None,
2307 ));
2308 assert!(!no_payload.verify());
2309 }
2310
2311 #[test]
2312 fn test_add_bond_invoice_wire_format_and_verify() {
2313 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2314 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
2315
2316 let msg = Message::Order(MessageKind::new(
2317 Some(uuid),
2318 Some(1),
2319 Some(2),
2320 Action::AddBondInvoice,
2321 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2322 ));
2323 assert!(msg.verify());
2324
2325 let json = msg.as_json().unwrap();
2327 assert!(
2328 json.contains("\"action\":\"add-bond-invoice\""),
2329 "expected kebab-case discriminator, got: {json}"
2330 );
2331
2332 let decoded = Message::from_json(&json).unwrap();
2334 assert!(decoded.verify());
2335 assert!(matches!(
2336 decoded.inner_action(),
2337 Some(Action::AddBondInvoice)
2338 ));
2339
2340 let no_id = Message::Order(MessageKind::new(
2342 None,
2343 Some(1),
2344 Some(2),
2345 Action::AddBondInvoice,
2346 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2347 ));
2348 assert!(!no_id.verify());
2349
2350 let wrong_payload = Message::Order(MessageKind::new(
2352 Some(uuid),
2353 Some(1),
2354 Some(2),
2355 Action::AddBondInvoice,
2356 Some(Payload::TextMessage("nope".to_string())),
2357 ));
2358 assert!(!wrong_payload.verify());
2359
2360 let no_payload = Message::Order(MessageKind::new(
2362 Some(uuid),
2363 Some(1),
2364 Some(2),
2365 Action::AddBondInvoice,
2366 None,
2367 ));
2368 assert!(!no_payload.verify());
2369
2370 if let Message::Order(kind) = &msg {
2372 assert_eq!(kind.get_payment_request(), Some(bolt11));
2373 } else {
2374 panic!("expected Message::Order");
2375 }
2376 }
2377
2378 #[test]
2379 fn test_restored_dispute_helper_serialization_roundtrip() {
2380 use crate::message::RestoredDisputeHelper;
2381
2382 let helper = RestoredDisputeHelper {
2383 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
2384 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
2385 dispute_status: "initiated".to_string(),
2386 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
2387 master_seller_pubkey: Some("npub1sellerkey".to_string()),
2388 trade_index_buyer: Some(1),
2389 trade_index_seller: Some(2),
2390 buyer_dispute: true,
2391 seller_dispute: false,
2392 solver_pubkey: None,
2393 };
2394
2395 let json = serde_json::to_string(&helper).unwrap();
2396 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
2397
2398 assert_eq!(deserialized.dispute_id, helper.dispute_id);
2399 assert_eq!(deserialized.order_id, helper.order_id);
2400 assert_eq!(deserialized.dispute_status, helper.dispute_status);
2401 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
2402 assert_eq!(
2403 deserialized.master_seller_pubkey,
2404 helper.master_seller_pubkey
2405 );
2406 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
2407 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
2408 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
2409 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
2410 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
2411
2412 let helper_seller_dispute = RestoredDisputeHelper {
2413 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
2414 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
2415 dispute_status: "in-progress".to_string(),
2416 master_buyer_pubkey: None,
2417 master_seller_pubkey: None,
2418 trade_index_buyer: None,
2419 trade_index_seller: None,
2420 buyer_dispute: false,
2421 seller_dispute: true,
2422 solver_pubkey: Some(
2423 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
2424 ),
2425 };
2426
2427 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
2428 let deserialized_seller: RestoredDisputeHelper =
2429 serde_json::from_str(&json_seller).unwrap();
2430
2431 assert_eq!(
2432 deserialized_seller.dispute_id,
2433 helper_seller_dispute.dispute_id
2434 );
2435 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
2436 assert_eq!(
2437 deserialized_seller.dispute_status,
2438 helper_seller_dispute.dispute_status
2439 );
2440 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
2441 assert_eq!(deserialized_seller.master_seller_pubkey, None);
2442 assert_eq!(deserialized_seller.trade_index_buyer, None);
2443 assert_eq!(deserialized_seller.trade_index_seller, None);
2444 assert!(!deserialized_seller.buyer_dispute);
2445 assert!(deserialized_seller.seller_dispute);
2446 assert_eq!(
2447 deserialized_seller.solver_pubkey,
2448 helper_seller_dispute.solver_pubkey
2449 );
2450 }
2451
2452 fn sample_lock_proof() -> CashuLockProof {
2453 CashuLockProof::new(
2454 "cashuAeyJ0b2tlbiI6dGVzdA".to_string(),
2455 "https://mint.example".to_string(),
2456 "02b_buyer".to_string(),
2457 "02s_seller".to_string(),
2458 "02m_mostro".to_string(),
2459 )
2460 }
2461
2462 #[test]
2463 fn test_cashu_lock_proof_json_round_trip() {
2464 let proof = sample_lock_proof();
2465 let json = proof.as_json().unwrap();
2466 let back = CashuLockProof::from_json(&json).unwrap();
2467 assert_eq!(back, proof);
2468 }
2469
2470 #[test]
2471 fn test_cashu_lock_proof_fee_token_round_trip() {
2472 let proof = sample_lock_proof().with_fee_token("cashuAfee".to_string());
2473 let json = proof.as_json().unwrap();
2474 assert!(json.contains("fee_token"));
2475 let back = CashuLockProof::from_json(&json).unwrap();
2476 assert_eq!(back, proof);
2477 assert_eq!(back.fee_token.as_deref(), Some("cashuAfee"));
2478 }
2479
2480 #[test]
2481 fn test_cashu_lock_proof_without_fee_token_is_omitted_and_defaults_to_none() {
2482 let proof = sample_lock_proof();
2485 assert_eq!(proof.fee_token, None);
2486 let json = proof.as_json().unwrap();
2487 assert!(!json.contains("fee_token"));
2488 let back = CashuLockProof::from_json(&json).unwrap();
2489 assert_eq!(back.fee_token, None);
2490 }
2491
2492 #[test]
2493 fn test_add_cashu_escrow_verifies_with_lock_proof() {
2494 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2495 let payload = Payload::CashuLockProof(sample_lock_proof());
2496 let kind = MessageKind::new(
2497 Some(order_id),
2498 None,
2499 None,
2500 Action::AddCashuEscrow,
2501 Some(payload),
2502 );
2503 assert!(
2504 kind.verify(),
2505 "CashuLockProof must verify on AddCashuEscrow"
2506 );
2507
2508 let json = Message::Order(kind).as_json().unwrap();
2511 assert!(json.contains("cashu_lock_proof"));
2512 assert!(Message::from_json(&json).unwrap().verify());
2513 }
2514
2515 #[test]
2516 fn test_add_cashu_escrow_requires_id_and_right_payload() {
2517 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2518
2519 let no_id = MessageKind::new(
2521 None,
2522 None,
2523 None,
2524 Action::AddCashuEscrow,
2525 Some(Payload::CashuLockProof(sample_lock_proof())),
2526 );
2527 assert!(
2528 !no_id.verify(),
2529 "AddCashuEscrow without id must be rejected"
2530 );
2531
2532 let wrong_payload = MessageKind::new(
2534 Some(order_id),
2535 None,
2536 None,
2537 Action::AddCashuEscrow,
2538 Some(Payload::TextMessage("not a lock proof".to_string())),
2539 );
2540 assert!(
2541 !wrong_payload.verify(),
2542 "AddCashuEscrow with non-lock-proof payload must be rejected"
2543 );
2544 }
2545
2546 #[test]
2547 fn test_cashu_pm_signature_verifies_with_signatures() {
2548 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2549 let kind = MessageKind::new(
2550 Some(order_id),
2551 None,
2552 None,
2553 Action::CashuPmSignature,
2554 Some(Payload::CashuSignatures(vec![
2555 CashuProofSignature::new("secret-0".to_string(), "deadbeef".to_string()),
2556 CashuProofSignature::new("secret-1".to_string(), "c0ffee".to_string()),
2557 ])),
2558 );
2559 assert!(
2560 kind.verify(),
2561 "CashuSignatures must verify on CashuPmSignature"
2562 );
2563
2564 let json = Message::Order(kind).as_json().unwrap();
2565 assert!(json.contains("cashu_signatures"));
2566 assert!(Message::from_json(&json).unwrap().verify());
2567
2568 let wrong = MessageKind::new(Some(order_id), None, None, Action::CashuPmSignature, None);
2570 assert!(
2571 !wrong.verify(),
2572 "CashuPmSignature without a signature payload must be rejected"
2573 );
2574
2575 let empty = MessageKind::new(
2578 Some(order_id),
2579 None,
2580 None,
2581 Action::CashuPmSignature,
2582 Some(Payload::CashuSignatures(vec![])),
2583 );
2584 assert!(
2585 !empty.verify(),
2586 "CashuPmSignature with an empty signature set must be rejected"
2587 );
2588 }
2589
2590 #[test]
2591 fn test_cashu_escrow_locked_is_informational() {
2592 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2593
2594 let ok = MessageKind::new(Some(order_id), None, None, Action::CashuEscrowLocked, None);
2596 assert!(ok.verify(), "CashuEscrowLocked with id must verify");
2597
2598 let no_id = MessageKind::new(None, None, None, Action::CashuEscrowLocked, None);
2600 assert!(
2601 !no_id.verify(),
2602 "CashuEscrowLocked without id must be rejected"
2603 );
2604 }
2605
2606 fn sample_declaration() -> PayerDeclaration {
2607 PayerDeclaration::new(payment_hash("EU|SEPA|DE89370400440532013000|ALICE SMITH"))
2608 }
2609
2610 fn sample_history() -> PaymentHistory {
2611 PaymentHistory {
2612 payment_hash: sample_declaration().payment_hash,
2613 buyer_mode: BuyerMode::Reputation,
2614 successful_trades: 47,
2615 distinct_counterparties: 29,
2616 experienced_counterparties: 11,
2617 first_success_at: Some(1_762_128_000),
2618 last_success_at: Some(1_787_654_321),
2619 }
2620 }
2621
2622 #[test]
2623 fn test_payer_history_actions_use_kebab_case_wire_names() {
2624 for (action, wire) in [
2625 (Action::DeclarePayer, "\"declare-payer\""),
2626 (Action::PayerDeclared, "\"payer-declared\""),
2627 (Action::PaymentHistory, "\"payment-history\""),
2628 ] {
2629 assert_eq!(serde_json::to_string(&action).unwrap(), wire);
2630 assert_eq!(serde_json::from_str::<Action>(wire).unwrap(), action);
2631 }
2632 }
2633
2634 #[test]
2635 fn test_declare_payer_requires_id_and_declaration() {
2636 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2637 let decl = Some(Payload::PayerDeclaration(sample_declaration()));
2638
2639 let ok = MessageKind::new(
2640 Some(order_id),
2641 Some(1),
2642 Some(7),
2643 Action::DeclarePayer,
2644 decl.clone(),
2645 );
2646 assert!(
2647 ok.verify(),
2648 "DeclarePayer with id and declaration must verify"
2649 );
2650
2651 let no_id = MessageKind::new(None, None, None, Action::DeclarePayer, decl);
2652 assert!(!no_id.verify(), "DeclarePayer without id must be rejected");
2653
2654 let no_payload = MessageKind::new(Some(order_id), None, None, Action::DeclarePayer, None);
2655 assert!(
2656 !no_payload.verify(),
2657 "DeclarePayer without payload must be rejected"
2658 );
2659
2660 let wrong = MessageKind::new(
2661 Some(order_id),
2662 None,
2663 None,
2664 Action::DeclarePayer,
2665 Some(Payload::TextMessage(
2666 "plaintext must never be sent".to_string(),
2667 )),
2668 );
2669 assert!(
2670 !wrong.verify(),
2671 "DeclarePayer with a non-declaration payload must be rejected"
2672 );
2673 }
2674
2675 #[test]
2676 fn test_payer_declared_requires_id_and_declaration() {
2677 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2678 let decl = Some(Payload::PayerDeclaration(sample_declaration()));
2679
2680 let ok = MessageKind::new(
2681 Some(order_id),
2682 None,
2683 None,
2684 Action::PayerDeclared,
2685 decl.clone(),
2686 );
2687 assert!(
2688 ok.verify(),
2689 "PayerDeclared with id and declaration must verify"
2690 );
2691
2692 let no_id = MessageKind::new(None, None, None, Action::PayerDeclared, decl);
2693 assert!(!no_id.verify(), "PayerDeclared without id must be rejected");
2694
2695 let no_payload = MessageKind::new(Some(order_id), None, None, Action::PayerDeclared, None);
2696 assert!(
2697 !no_payload.verify(),
2698 "PayerDeclared without payload must be rejected"
2699 );
2700
2701 let wrong = MessageKind::new(
2702 Some(order_id),
2703 None,
2704 None,
2705 Action::PayerDeclared,
2706 Some(Payload::PaymentHistory(sample_history())),
2707 );
2708 assert!(
2709 !wrong.verify(),
2710 "PayerDeclared with a history payload must be rejected"
2711 );
2712 }
2713
2714 #[test]
2715 fn test_payment_history_accepts_query_and_reply_shapes() {
2716 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2717
2718 let query = MessageKind::new(Some(order_id), Some(3), None, Action::PaymentHistory, None);
2720 assert!(
2721 query.verify(),
2722 "PaymentHistory query (no payload) must verify"
2723 );
2724
2725 let reply = MessageKind::new(
2727 Some(order_id),
2728 None,
2729 None,
2730 Action::PaymentHistory,
2731 Some(Payload::PaymentHistory(sample_history())),
2732 );
2733 assert!(reply.verify(), "PaymentHistory reply must verify");
2734
2735 let no_id = MessageKind::new(None, None, None, Action::PaymentHistory, None);
2736 assert!(
2737 !no_id.verify(),
2738 "PaymentHistory without id must be rejected"
2739 );
2740
2741 let wrong = MessageKind::new(
2742 Some(order_id),
2743 None,
2744 None,
2745 Action::PaymentHistory,
2746 Some(Payload::PayerDeclaration(sample_declaration())),
2747 );
2748 assert!(
2749 !wrong.verify(),
2750 "PaymentHistory with a declaration payload must be rejected"
2751 );
2752 }
2753
2754 #[test]
2755 fn test_payer_payloads_round_trip_with_snake_case_keys() {
2756 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2757
2758 let declare = Message::Order(MessageKind::new(
2759 Some(order_id),
2760 Some(981_231),
2761 Some(7),
2762 Action::DeclarePayer,
2763 Some(Payload::PayerDeclaration(sample_declaration())),
2764 ));
2765 let json = declare.as_json().unwrap();
2766 assert!(json.contains("\"action\":\"declare-payer\""));
2767 assert!(json.contains("\"payer_declaration\":{\"payment_hash\":"));
2768 assert!(Message::from_json(&json).unwrap().verify());
2769
2770 let push = Message::Order(MessageKind::new(
2771 Some(order_id),
2772 None,
2773 None,
2774 Action::PaymentHistory,
2775 Some(Payload::PaymentHistory(sample_history())),
2776 ));
2777 let json = push.as_json().unwrap();
2778 assert!(json.contains("\"payment_history\":{"));
2779 assert!(json.contains("\"buyer_mode\":\"reputation\""));
2780 assert!(json.contains("\"experienced_counterparties\":11"));
2781 let back = Message::from_json(&json).unwrap();
2782 assert!(back.verify());
2783 match back.get_inner_message_kind().payload.clone() {
2784 Some(Payload::PaymentHistory(h)) => assert_eq!(h, sample_history()),
2785 other => panic!("expected a PaymentHistory payload, got {other:?}"),
2786 }
2787 }
2788
2789 #[test]
2790 fn test_payment_history_parses_the_protocol_wire_example() {
2791 let hash = sample_declaration().payment_hash;
2794 let json = format!(
2795 r#"{{"payment_history": {{"payment_hash": "{hash}", "buyer_mode": "full_privacy",
2796 "successful_trades": 0, "distinct_counterparties": 0,
2797 "experienced_counterparties": 0,
2798 "first_success_at": null, "last_success_at": null}}}}"#
2799 );
2800 let payload: Payload = serde_json::from_str(&json).unwrap();
2801 match payload {
2802 Payload::PaymentHistory(h) => assert_eq!(h, PaymentHistory::unavailable(hash)),
2803 other => panic!("expected a PaymentHistory payload, got {other:?}"),
2804 }
2805 }
2806
2807 #[test]
2810 fn reputation_messages_have_the_documented_wire_shape() {
2811 let export = Message::new_order(
2812 None,
2813 Some(4126),
2814 None,
2815 Action::ExportReputation,
2816 Some(Payload::ReputationExportRequest(
2817 super::ReputationExportRequest {
2818 destination: "ab".repeat(32),
2819 rebind: None,
2820 },
2821 )),
2822 );
2823 let json: serde_json::Value = serde_json::from_str(&export.as_json().unwrap()).unwrap();
2824 assert_eq!(
2825 json,
2826 serde_json::json!({ "order": {
2827 "version": 2,
2828 "request_id": 4126,
2829 "trade_index": null,
2830 "action": "export-reputation",
2831 "payload": { "reputation_export_request": {
2832 "destination": "ab".repeat(32),
2833 "rebind": null,
2834 }},
2835 }})
2836 );
2837
2838 for (action, wire) in [
2839 (Action::ReputationExported, "reputation-exported"),
2840 (Action::ImportReputation, "import-reputation"),
2841 ] {
2842 let message = Message::new_order(
2843 None,
2844 None,
2845 None,
2846 action,
2847 Some(Payload::ReputationAttestation("{}".into())),
2848 );
2849 let json: serde_json::Value =
2850 serde_json::from_str(&message.as_json().unwrap()).unwrap();
2851 assert_eq!(json["order"]["action"], wire);
2852 assert_eq!(json["order"]["payload"]["reputation_attestation"], "{}");
2853 }
2854
2855 let imported = Message::new_order(None, None, None, Action::ReputationImported, None);
2856 let json: serde_json::Value = serde_json::from_str(&imported.as_json().unwrap()).unwrap();
2857 assert_eq!(json["order"]["action"], "reputation-imported");
2858 assert!(json["order"]["payload"].is_null());
2859 }
2860
2861 #[test]
2862 fn an_export_request_without_rebind_deserialises() {
2863 let json = r#"{"order":{"version":2,"action":"export-reputation","payload":{"reputation_export_request":{"destination":"ab"}}}}"#;
2864 let message = Message::from_json(json).unwrap();
2865 let kind = message.get_inner_message_kind();
2866 match &kind.payload {
2867 Some(Payload::ReputationExportRequest(request)) => assert_eq!(request.rebind, None),
2868 other => panic!("unexpected payload {other:?}"),
2869 }
2870 assert!(kind.verify());
2871 }
2872
2873 #[test]
2874 fn reputation_actions_verify_only_without_an_id_and_with_their_payload() {
2875 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2876 let request = Payload::ReputationExportRequest(super::ReputationExportRequest {
2877 destination: "ab".repeat(32),
2878 rebind: None,
2879 });
2880 let attestation = Payload::ReputationAttestation("{}".into());
2881 let cases = [
2882 (Action::ExportReputation, Some(request.clone()), true),
2883 (Action::ExportReputation, Some(attestation.clone()), false),
2884 (Action::ExportReputation, None, false),
2885 (Action::ReputationExported, Some(attestation.clone()), true),
2886 (Action::ReputationExported, None, false),
2887 (Action::ImportReputation, Some(attestation.clone()), true),
2888 (Action::ImportReputation, Some(request.clone()), false),
2889 (Action::ReputationImported, None, true),
2890 (Action::ReputationImported, Some(attestation.clone()), false),
2891 ];
2892 for (action, payload, ok) in cases {
2893 let without_id = MessageKind::new(None, None, None, action.clone(), payload.clone());
2894 assert_eq!(without_id.verify(), ok, "{action:?} {payload:?}");
2895 let with_id = MessageKind::new(Some(order_id), None, None, action.clone(), payload);
2896 assert!(!with_id.verify(), "{action:?} must not carry an id");
2897 }
2898 }
2899
2900 #[test]
2907 fn reputation_messages_keep_protocol_version_2() {
2908 for action in [
2909 Action::ExportReputation,
2910 Action::ReputationExported,
2911 Action::ImportReputation,
2912 Action::ReputationImported,
2913 ] {
2914 assert_eq!(MessageKind::new(None, None, None, action, None).version, 2);
2915 }
2916 }
2917}