1use crate::prelude::*;
14use bitcoin::hashes::sha256::Hash as Sha256Hash;
15use bitcoin::hashes::Hash;
16use nostr_sdk::prelude::*;
17use secp256k1::schnorr;
18use secp256k1::Secp256k1;
19#[cfg(feature = "sqlx")]
20use sqlx::FromRow;
21
22use std::fmt;
23use uuid::Uuid;
24
25#[derive(Debug, Deserialize, Serialize, Clone)]
32pub struct Peer {
33 pub pubkey: String,
35 pub reputation: Option<UserInfo>,
38}
39
40impl Peer {
41 pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
43 Self { pubkey, reputation }
44 }
45
46 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
48 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
49 }
50
51 pub fn as_json(&self) -> Result<String, ServiceError> {
53 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
54 }
55}
56
57#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
63#[serde(rename_all = "kebab-case")]
64pub enum Action {
65 NewOrder,
67 TakeSell,
70 TakeBuy,
72 PayInvoice,
75 PayBondInvoice,
82 FiatSent,
84 FiatSentOk,
86 Release,
88 Released,
90 Cancel,
92 Canceled,
94 CooperativeCancelInitiatedByYou,
96 CooperativeCancelInitiatedByPeer,
98 DisputeInitiatedByYou,
100 DisputeInitiatedByPeer,
102 CooperativeCancelAccepted,
104 BuyerInvoiceAccepted,
106 BondInvoiceAccepted,
112 PurchaseCompleted,
114 BondPayoutCompleted,
120 BondSlashed,
128 HoldInvoicePaymentAccepted,
130 HoldInvoicePaymentSettled,
132 HoldInvoicePaymentCanceled,
134 WaitingSellerToPay,
136 WaitingBuyerInvoice,
138 AddInvoice,
141 AddBondInvoice,
148 BuyerTookOrder,
150 Rate,
152 RateUser,
154 RateReceived,
156 CantDo,
158 Dispute,
160 AdminCancel,
162 AdminCanceled,
164 AdminSettle,
166 AdminSettled,
168 AdminAddSolver,
170 AdminTakeDispute,
172 AdminTookDispute,
174 PaymentFailed,
177 InvoiceUpdated,
179 SendDm,
181 TradePubkey,
183 RestoreSession,
185 LastTradeIndex,
188 Orders,
191 AddCashuEscrow,
196 CashuEscrowLocked,
201 CashuPmSignature,
207}
208
209impl fmt::Display for Action {
210 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
211 write!(f, "{self:?}")
212 }
213}
214
215#[derive(Debug, Clone, Deserialize, Serialize)]
222#[serde(rename_all = "kebab-case")]
223pub enum Message {
224 Order(MessageKind),
226 Dispute(MessageKind),
228 CantDo(MessageKind),
230 Rate(MessageKind),
232 Dm(MessageKind),
234 Restore(MessageKind),
236}
237
238impl Message {
239 pub fn new_order(
242 id: Option<Uuid>,
243 request_id: Option<u64>,
244 trade_index: Option<i64>,
245 action: Action,
246 payload: Option<Payload>,
247 ) -> Self {
248 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
249 Self::Order(kind)
250 }
251
252 pub fn new_dispute(
255 id: Option<Uuid>,
256 request_id: Option<u64>,
257 trade_index: Option<i64>,
258 action: Action,
259 payload: Option<Payload>,
260 ) -> Self {
261 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
262
263 Self::Dispute(kind)
264 }
265
266 pub fn new_restore(payload: Option<Payload>) -> Self {
271 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
272 Self::Restore(kind)
273 }
274
275 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
278 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
279
280 Self::CantDo(kind)
281 }
282
283 pub fn new_dm(
285 id: Option<Uuid>,
286 request_id: Option<u64>,
287 action: Action,
288 payload: Option<Payload>,
289 ) -> Self {
290 let kind = MessageKind::new(id, request_id, None, action, payload);
291
292 Self::Dm(kind)
293 }
294
295 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
297 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
298 }
299
300 pub fn as_json(&self) -> Result<String, ServiceError> {
302 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
303 }
304
305 pub fn get_inner_message_kind(&self) -> &MessageKind {
307 match self {
308 Message::Dispute(k)
309 | Message::Order(k)
310 | Message::CantDo(k)
311 | Message::Rate(k)
312 | Message::Dm(k)
313 | Message::Restore(k) => k,
314 }
315 }
316
317 pub fn inner_action(&self) -> Option<Action> {
322 match self {
323 Message::Dispute(a)
324 | Message::Order(a)
325 | Message::CantDo(a)
326 | Message::Rate(a)
327 | Message::Dm(a)
328 | Message::Restore(a) => Some(a.get_action()),
329 }
330 }
331
332 pub fn verify(&self) -> bool {
335 match self {
336 Message::Order(m)
337 | Message::Dispute(m)
338 | Message::CantDo(m)
339 | Message::Rate(m)
340 | Message::Dm(m)
341 | Message::Restore(m) => m.verify(),
342 }
343 }
344
345 pub fn sign(message: String, keys: &Keys) -> Signature {
357 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
358 keys.sign_schnorr(hash.to_byte_array())
359 }
360
361 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
371 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
372 let hash = hash.to_byte_array();
373
374 let secp = Secp256k1::verification_only();
375 if let Ok(xonlykey) = pubkey.xonly() {
376 let sig = schnorr::Signature::from_byte_array(*sig.as_bytes());
377 xonlykey.verify(&secp, &hash, &sig).is_ok()
378 } else {
379 false
380 }
381 }
382}
383
384#[derive(Debug, Clone, Deserialize, Serialize)]
391pub struct MessageKind {
392 pub version: u8,
395 pub request_id: Option<u64>,
398 pub trade_index: Option<i64>,
401 #[serde(skip_serializing_if = "Option::is_none")]
404 pub id: Option<Uuid>,
405 pub action: Action,
407 pub payload: Option<Payload>,
410}
411
412type Amount = i64;
414
415#[derive(Debug, Deserialize, Serialize, Clone)]
420pub struct PaymentFailedInfo {
421 pub payment_attempts: u32,
423 pub payment_retries_interval: u32,
425}
426
427#[cfg_attr(feature = "sqlx", derive(FromRow))]
432#[derive(Debug, Deserialize, Serialize, Clone)]
433pub struct RestoredOrderHelper {
434 pub id: Uuid,
436 pub status: String,
438 pub master_buyer_pubkey: Option<String>,
440 pub master_seller_pubkey: Option<String>,
442 pub trade_index_buyer: Option<i64>,
444 pub trade_index_seller: Option<i64>,
446}
447
448#[cfg_attr(feature = "sqlx", derive(FromRow))]
453#[derive(Debug, Deserialize, Serialize, Clone)]
454pub struct RestoredDisputeHelper {
455 pub dispute_id: Uuid,
457 pub order_id: Uuid,
459 pub dispute_status: String,
461 pub master_buyer_pubkey: Option<String>,
463 pub master_seller_pubkey: Option<String>,
465 pub trade_index_buyer: Option<i64>,
467 pub trade_index_seller: Option<i64>,
469 pub buyer_dispute: bool,
473 pub seller_dispute: bool,
477 pub solver_pubkey: Option<String>,
480}
481
482#[cfg_attr(feature = "sqlx", derive(FromRow))]
484#[derive(Debug, Deserialize, Serialize, Clone)]
485pub struct RestoredOrdersInfo {
486 pub order_id: Uuid,
488 pub trade_index: i64,
490 pub status: String,
492 #[serde(default)]
506 pub counterparty_trade_pubkey: Option<String>,
507}
508
509#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
511#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
512#[serde(rename_all = "lowercase")]
513#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
514pub enum DisputeInitiator {
515 Buyer,
517 Seller,
519}
520
521#[cfg_attr(feature = "sqlx", derive(FromRow))]
523#[derive(Debug, Deserialize, Serialize, Clone)]
524pub struct RestoredDisputesInfo {
525 pub dispute_id: Uuid,
527 pub order_id: Uuid,
529 pub trade_index: i64,
531 pub status: String,
533 pub initiator: Option<DisputeInitiator>,
536 pub solver_pubkey: Option<String>,
539}
540
541#[derive(Debug, Deserialize, Serialize, Clone, Default)]
546pub struct RestoreSessionInfo {
547 #[serde(rename = "orders")]
549 pub restore_orders: Vec<RestoredOrdersInfo>,
550 #[serde(rename = "disputes")]
552 pub restore_disputes: Vec<RestoredDisputesInfo>,
553}
554
555#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
563pub struct BondResolution {
564 pub slash_seller: bool,
566 pub slash_buyer: bool,
568}
569
570#[derive(Debug, Deserialize, Serialize, Clone)]
585pub struct BondPayoutRequest {
586 pub order: SmallOrder,
590 pub slashed_at: i64,
595}
596
597#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
606pub struct CashuLockProof {
607 pub token: String,
609 pub mint_url: String,
612 pub buyer_pubkey: String,
614 pub seller_pubkey: String,
616 pub mostro_pubkey: String,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub fee_token: Option<String>,
625}
626
627impl CashuLockProof {
628 pub fn new(
630 token: String,
631 mint_url: String,
632 buyer_pubkey: String,
633 seller_pubkey: String,
634 mostro_pubkey: String,
635 ) -> Self {
636 Self {
637 token,
638 mint_url,
639 buyer_pubkey,
640 seller_pubkey,
641 mostro_pubkey,
642 fee_token: None,
643 }
644 }
645
646 pub fn with_fee_token(mut self, fee_token: String) -> Self {
649 self.fee_token = Some(fee_token);
650 self
651 }
652
653 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
655 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
656 }
657
658 pub fn as_json(&self) -> Result<String, ServiceError> {
660 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
661 }
662}
663
664#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
673pub struct CashuProofSignature {
674 pub secret: String,
678 pub signature: String,
681}
682
683impl CashuProofSignature {
684 pub fn new(secret: String, signature: String) -> Self {
686 Self { secret, signature }
687 }
688}
689
690#[derive(Debug, Deserialize, Serialize, Clone)]
696#[serde(rename_all = "snake_case")]
697pub enum Payload {
698 Order(SmallOrder),
700 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
708 TextMessage(String),
710 Peer(Peer),
712 RatingUser(u8),
714 Amount(Amount),
716 Dispute(Uuid, Option<SolverDisputeInfo>),
719 CantDo(Option<CantDoReason>),
721 NextTrade(String, u32),
724 PaymentFailed(PaymentFailedInfo),
726 RestoreData(RestoreSessionInfo),
728 Ids(Vec<Uuid>),
730 Orders(Vec<SmallOrder>),
732 BondResolution(BondResolution),
735 BondPayoutRequest(BondPayoutRequest),
741 CashuLockProof(CashuLockProof),
744 CashuSignatures(Vec<CashuProofSignature>),
750}
751
752#[allow(dead_code)]
753impl MessageKind {
754 pub fn new(
757 id: Option<Uuid>,
758 request_id: Option<u64>,
759 trade_index: Option<i64>,
760 action: Action,
761 payload: Option<Payload>,
762 ) -> Self {
763 Self {
764 version: PROTOCOL_VER,
765 request_id,
766 trade_index,
767 id,
768 action,
769 payload,
770 }
771 }
772 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
774 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
775 }
776 pub fn as_json(&self) -> Result<String, ServiceError> {
778 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
779 }
780
781 pub fn get_action(&self) -> Action {
783 self.action.clone()
784 }
785
786 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
793 match &self.payload {
794 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
795 None => Ok(None),
796 _ => Err(ServiceError::InvalidPayload),
797 }
798 }
799
800 pub fn get_rating(&self) -> Result<u8, ServiceError> {
808 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
809 if !(MIN_RATING..=MAX_RATING).contains(&v) {
810 return Err(ServiceError::InvalidRatingValue);
811 }
812 Ok(v)
813 } else {
814 Err(ServiceError::InvalidRating)
815 }
816 }
817
818 pub fn verify(&self) -> bool {
825 match &self.action {
826 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
827 Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
828 if self.id.is_none() {
829 return false;
830 }
831 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
832 }
833 Action::AddBondInvoice => {
834 if self.id.is_none() {
835 return false;
836 }
837 matches!(
843 &self.payload,
844 Some(Payload::BondPayoutRequest(_)) | Some(Payload::PaymentRequest(_, _, _))
845 )
846 }
847 Action::AdminSettle | Action::AdminCancel => {
848 if self.id.is_none() {
849 return false;
850 }
851 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
852 }
853 Action::AddCashuEscrow => {
854 if self.id.is_none() {
855 return false;
856 }
857 matches!(&self.payload, Some(Payload::CashuLockProof(_)))
858 }
859 Action::CashuPmSignature => {
860 if self.id.is_none() {
861 return false;
862 }
863 matches!(&self.payload, Some(Payload::CashuSignatures(sigs)) if !sigs.is_empty())
864 }
865 Action::TakeSell
866 | Action::TakeBuy
867 | Action::FiatSent
868 | Action::FiatSentOk
869 | Action::Release
870 | Action::Released
871 | Action::Dispute
872 | Action::AdminCanceled
873 | Action::AdminSettled
874 | Action::Rate
875 | Action::RateReceived
876 | Action::AdminTakeDispute
877 | Action::AdminTookDispute
878 | Action::DisputeInitiatedByYou
879 | Action::DisputeInitiatedByPeer
880 | Action::WaitingBuyerInvoice
881 | Action::PurchaseCompleted
882 | Action::BondPayoutCompleted
883 | Action::BondSlashed
884 | Action::HoldInvoicePaymentAccepted
885 | Action::HoldInvoicePaymentSettled
886 | Action::HoldInvoicePaymentCanceled
887 | Action::WaitingSellerToPay
888 | Action::BuyerTookOrder
889 | Action::BuyerInvoiceAccepted
890 | Action::BondInvoiceAccepted
891 | Action::CooperativeCancelInitiatedByYou
892 | Action::CooperativeCancelInitiatedByPeer
893 | Action::CooperativeCancelAccepted
894 | Action::Cancel
895 | Action::InvoiceUpdated
896 | Action::AdminAddSolver
897 | Action::SendDm
898 | Action::TradePubkey
899 | Action::CashuEscrowLocked
900 | Action::Canceled => {
901 if self.id.is_none() {
902 return false;
903 }
904 !matches!(
905 &self.payload,
906 Some(Payload::BondResolution(_)) | Some(Payload::BondPayoutRequest(_))
907 )
908 }
909 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
910 Action::PaymentFailed => {
911 if self.id.is_none() {
912 return false;
913 }
914 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
915 }
916 Action::RateUser => {
917 matches!(&self.payload, Some(Payload::RatingUser(_)))
918 }
919 Action::CantDo => {
920 matches!(&self.payload, Some(Payload::CantDo(_)))
921 }
922 Action::Orders => {
923 matches!(
924 &self.payload,
925 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
926 )
927 }
928 }
929 }
930
931 pub fn get_order(&self) -> Option<&SmallOrder> {
936 if self.action != Action::NewOrder {
937 return None;
938 }
939 match &self.payload {
940 Some(Payload::Order(o)) => Some(o),
941 _ => None,
942 }
943 }
944
945 pub fn get_payment_request(&self) -> Option<String> {
952 if self.action != Action::TakeSell
953 && self.action != Action::AddInvoice
954 && self.action != Action::AddBondInvoice
955 && self.action != Action::NewOrder
956 {
957 return None;
958 }
959 match &self.payload {
960 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
961 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
962 _ => None,
963 }
964 }
965
966 pub fn get_amount(&self) -> Option<Amount> {
970 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
971 return None;
972 }
973 match &self.payload {
974 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
975 Some(Payload::Amount(amount)) => Some(*amount),
976 _ => None,
977 }
978 }
979
980 pub fn get_payload(&self) -> Option<&Payload> {
982 self.payload.as_ref()
983 }
984
985 pub fn has_trade_index(&self) -> (bool, i64) {
988 if let Some(index) = self.trade_index {
989 return (true, index);
990 }
991 (false, 0)
992 }
993
994 pub fn trade_index(&self) -> i64 {
996 if let Some(index) = self.trade_index {
997 return index;
998 }
999 0
1000 }
1001}
1002
1003#[cfg(test)]
1004mod test {
1005 use crate::message::{
1006 Action, BondPayoutRequest, CashuLockProof, CashuProofSignature, Message, MessageKind,
1007 Payload, Peer,
1008 };
1009 use crate::order::SmallOrder;
1010 use crate::user::UserInfo;
1011 use nostr_sdk::prelude::Keys;
1012 use uuid::uuid;
1013
1014 #[test]
1015 fn test_peer_with_reputation() {
1016 let reputation = UserInfo {
1018 rating: 4.5,
1019 reviews: 10,
1020 operating_days: 30,
1021 since: None,
1022 };
1023 let peer = Peer::new(
1024 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1025 Some(reputation.clone()),
1026 );
1027
1028 assert_eq!(
1030 peer.pubkey,
1031 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1032 );
1033 assert!(peer.reputation.is_some());
1034 let peer_reputation = peer.reputation.clone().unwrap();
1035 assert_eq!(peer_reputation.rating, 4.5);
1036 assert_eq!(peer_reputation.reviews, 10);
1037 assert_eq!(peer_reputation.operating_days, 30);
1038
1039 let json = peer.as_json().unwrap();
1041 let deserialized_peer = Peer::from_json(&json).unwrap();
1042 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1043 assert!(deserialized_peer.reputation.is_some());
1044 let deserialized_reputation = deserialized_peer.reputation.unwrap();
1045 assert_eq!(deserialized_reputation.rating, 4.5);
1046 assert_eq!(deserialized_reputation.reviews, 10);
1047 assert_eq!(deserialized_reputation.operating_days, 30);
1048 }
1049
1050 #[test]
1051 fn test_peer_without_reputation() {
1052 let peer = Peer::new(
1054 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1055 None,
1056 );
1057
1058 assert_eq!(
1060 peer.pubkey,
1061 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1062 );
1063 assert!(peer.reputation.is_none());
1064
1065 let json = peer.as_json().unwrap();
1067 let deserialized_peer = Peer::from_json(&json).unwrap();
1068 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1069 assert!(deserialized_peer.reputation.is_none());
1070 }
1071
1072 #[test]
1073 fn test_peer_in_message() {
1074 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1075
1076 let reputation = UserInfo {
1078 rating: 4.5,
1079 reviews: 10,
1080 operating_days: 30,
1081 since: None,
1082 };
1083 let peer_with_reputation = Peer::new(
1084 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1085 Some(reputation),
1086 );
1087 let payload_with_reputation = Payload::Peer(peer_with_reputation);
1088 let message_with_reputation = Message::Order(MessageKind::new(
1089 Some(uuid),
1090 Some(1),
1091 Some(2),
1092 Action::FiatSentOk,
1093 Some(payload_with_reputation),
1094 ));
1095
1096 assert!(message_with_reputation.verify());
1098 let message_json = message_with_reputation.as_json().unwrap();
1099 let deserialized_message = Message::from_json(&message_json).unwrap();
1100 assert!(deserialized_message.verify());
1101
1102 let peer_without_reputation = Peer::new(
1104 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1105 None,
1106 );
1107 let payload_without_reputation = Payload::Peer(peer_without_reputation);
1108 let message_without_reputation = Message::Order(MessageKind::new(
1109 Some(uuid),
1110 Some(1),
1111 Some(2),
1112 Action::FiatSentOk,
1113 Some(payload_without_reputation),
1114 ));
1115
1116 assert!(message_without_reputation.verify());
1118 let message_json = message_without_reputation.as_json().unwrap();
1119 let deserialized_message = Message::from_json(&message_json).unwrap();
1120 assert!(deserialized_message.verify());
1121 }
1122
1123 #[test]
1124 fn test_bond_payout_request_payload_verifies_on_add_bond_invoice() {
1125 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1126 let order = SmallOrder {
1127 id: Some(order_id),
1128 kind: None,
1129 status: None,
1130 amount: 500,
1131 fiat_code: "USD".to_string(),
1132 min_amount: None,
1133 max_amount: None,
1134 fiat_amount: 0,
1135 payment_method: "lightning".to_string(),
1136 premium: 0,
1137 buyer_trade_pubkey: None,
1138 seller_trade_pubkey: None,
1139 buyer_invoice: None,
1140 created_at: None,
1141 expires_at: None,
1142 };
1143 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1144 order,
1145 slashed_at: 1_734_000_000,
1146 });
1147 let kind = MessageKind::new(
1148 Some(order_id),
1149 None,
1150 None,
1151 Action::AddBondInvoice,
1152 Some(payload),
1153 );
1154 assert!(
1155 kind.verify(),
1156 "BondPayoutRequest must verify on AddBondInvoice"
1157 );
1158
1159 let m = Message::Order(kind);
1162 let json = m.as_json().unwrap();
1163 assert!(json.contains("bond_payout_request"));
1164 let back = Message::from_json(&json).unwrap();
1165 assert!(back.verify());
1166 }
1167
1168 #[test]
1169 fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1170 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1173 let order = SmallOrder {
1174 id: Some(order_id),
1175 kind: None,
1176 status: None,
1177 amount: 500,
1178 fiat_code: "USD".to_string(),
1179 min_amount: None,
1180 max_amount: None,
1181 fiat_amount: 0,
1182 payment_method: "lightning".to_string(),
1183 premium: 0,
1184 buyer_trade_pubkey: None,
1185 seller_trade_pubkey: None,
1186 buyer_invoice: None,
1187 created_at: None,
1188 expires_at: None,
1189 };
1190
1191 let _exhaustive: fn(Action) = |a| match a {
1196 Action::AddBondInvoice => {}
1197 Action::NewOrder
1198 | Action::TakeSell
1199 | Action::TakeBuy
1200 | Action::PayInvoice
1201 | Action::PayBondInvoice
1202 | Action::FiatSent
1203 | Action::FiatSentOk
1204 | Action::Release
1205 | Action::Released
1206 | Action::Cancel
1207 | Action::Canceled
1208 | Action::CooperativeCancelInitiatedByYou
1209 | Action::CooperativeCancelInitiatedByPeer
1210 | Action::DisputeInitiatedByYou
1211 | Action::DisputeInitiatedByPeer
1212 | Action::CooperativeCancelAccepted
1213 | Action::BuyerInvoiceAccepted
1214 | Action::BondInvoiceAccepted
1215 | Action::PurchaseCompleted
1216 | Action::BondPayoutCompleted
1217 | Action::BondSlashed
1218 | Action::HoldInvoicePaymentAccepted
1219 | Action::HoldInvoicePaymentSettled
1220 | Action::HoldInvoicePaymentCanceled
1221 | Action::WaitingSellerToPay
1222 | Action::WaitingBuyerInvoice
1223 | Action::AddInvoice
1224 | Action::BuyerTookOrder
1225 | Action::Rate
1226 | Action::RateUser
1227 | Action::RateReceived
1228 | Action::CantDo
1229 | Action::Dispute
1230 | Action::AdminCancel
1231 | Action::AdminCanceled
1232 | Action::AdminSettle
1233 | Action::AdminSettled
1234 | Action::AdminAddSolver
1235 | Action::AdminTakeDispute
1236 | Action::AdminTookDispute
1237 | Action::PaymentFailed
1238 | Action::InvoiceUpdated
1239 | Action::SendDm
1240 | Action::TradePubkey
1241 | Action::RestoreSession
1242 | Action::LastTradeIndex
1243 | Action::AddCashuEscrow
1244 | Action::CashuEscrowLocked
1245 | Action::CashuPmSignature
1246 | Action::Orders => {}
1247 };
1248
1249 let other_actions: &[Action] = &[
1250 Action::NewOrder,
1251 Action::TakeSell,
1252 Action::TakeBuy,
1253 Action::PayInvoice,
1254 Action::PayBondInvoice,
1255 Action::FiatSent,
1256 Action::FiatSentOk,
1257 Action::Release,
1258 Action::Released,
1259 Action::Cancel,
1260 Action::Canceled,
1261 Action::CooperativeCancelInitiatedByYou,
1262 Action::CooperativeCancelInitiatedByPeer,
1263 Action::DisputeInitiatedByYou,
1264 Action::DisputeInitiatedByPeer,
1265 Action::CooperativeCancelAccepted,
1266 Action::BuyerInvoiceAccepted,
1267 Action::BondInvoiceAccepted,
1268 Action::PurchaseCompleted,
1269 Action::BondPayoutCompleted,
1270 Action::BondSlashed,
1271 Action::HoldInvoicePaymentAccepted,
1272 Action::HoldInvoicePaymentSettled,
1273 Action::HoldInvoicePaymentCanceled,
1274 Action::WaitingSellerToPay,
1275 Action::WaitingBuyerInvoice,
1276 Action::AddInvoice,
1277 Action::BuyerTookOrder,
1278 Action::Rate,
1279 Action::RateUser,
1280 Action::RateReceived,
1281 Action::CantDo,
1282 Action::Dispute,
1283 Action::AdminCancel,
1284 Action::AdminCanceled,
1285 Action::AdminSettle,
1286 Action::AdminSettled,
1287 Action::AdminAddSolver,
1288 Action::AdminTakeDispute,
1289 Action::AdminTookDispute,
1290 Action::PaymentFailed,
1291 Action::InvoiceUpdated,
1292 Action::SendDm,
1293 Action::TradePubkey,
1294 Action::RestoreSession,
1295 Action::LastTradeIndex,
1296 Action::Orders,
1297 Action::AddCashuEscrow,
1298 Action::CashuEscrowLocked,
1299 Action::CashuPmSignature,
1300 ];
1301
1302 for action in other_actions {
1303 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1304 order: order.clone(),
1305 slashed_at: 0,
1306 });
1307 let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1308 assert!(
1309 !kind.verify(),
1310 "BondPayoutRequest must be rejected on {action:?}"
1311 );
1312 }
1313 }
1314
1315 #[test]
1316 fn test_bond_payout_ack_actions_verify_and_wire_format() {
1317 use crate::message::BondResolution;
1318
1319 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1320
1321 let order = || SmallOrder {
1324 id: Some(order_id),
1325 kind: None,
1326 status: None,
1327 amount: 500,
1328 fiat_code: "USD".to_string(),
1329 min_amount: None,
1330 max_amount: None,
1331 fiat_amount: 0,
1332 payment_method: "lightning".to_string(),
1333 premium: 0,
1334 buyer_trade_pubkey: None,
1335 seller_trade_pubkey: None,
1336 buyer_invoice: None,
1337 created_at: None,
1338 expires_at: None,
1339 };
1340
1341 for (action, discriminator) in [
1347 (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1348 (Action::BondPayoutCompleted, "bond-payout-completed"),
1349 (Action::BondSlashed, "bond-slashed"),
1350 ] {
1351 let ok = Message::Order(MessageKind::new(
1353 Some(order_id),
1354 Some(1),
1355 Some(2),
1356 action.clone(),
1357 Some(Payload::Order(order())),
1358 ));
1359 assert!(ok.verify(), "{action:?} + Order should verify");
1360
1361 let no_id = Message::Order(MessageKind::new(
1363 None,
1364 Some(1),
1365 Some(2),
1366 action.clone(),
1367 Some(Payload::Order(order())),
1368 ));
1369 assert!(!no_id.verify(), "{action:?} without id must be rejected");
1370
1371 let with_resolution = Message::Order(MessageKind::new(
1373 Some(order_id),
1374 Some(1),
1375 Some(2),
1376 action.clone(),
1377 Some(Payload::BondResolution(BondResolution {
1378 slash_seller: true,
1379 slash_buyer: false,
1380 })),
1381 ));
1382 assert!(
1383 !with_resolution.verify(),
1384 "{action:?} + BondResolution must be rejected"
1385 );
1386
1387 let with_request = Message::Order(MessageKind::new(
1389 Some(order_id),
1390 Some(1),
1391 Some(2),
1392 action.clone(),
1393 Some(Payload::BondPayoutRequest(BondPayoutRequest {
1394 order: order(),
1395 slashed_at: 0,
1396 })),
1397 ));
1398 assert!(
1399 !with_request.verify(),
1400 "{action:?} + BondPayoutRequest must be rejected"
1401 );
1402
1403 let json = ok.as_json().unwrap();
1405 assert!(
1406 json.contains(&format!("\"action\":\"{discriminator}\"")),
1407 "expected kebab-case discriminator {discriminator}, got: {json}"
1408 );
1409 let decoded = Message::from_json(&json).unwrap();
1410 assert!(decoded.verify());
1411 assert_eq!(decoded.inner_action(), Some(action));
1412 }
1413 }
1414
1415 #[test]
1416 fn test_payment_failed_payload() {
1417 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1418
1419 let payment_failed_info = crate::message::PaymentFailedInfo {
1421 payment_attempts: 3,
1422 payment_retries_interval: 60,
1423 };
1424
1425 let payload = Payload::PaymentFailed(payment_failed_info);
1426 let message = Message::Order(MessageKind::new(
1427 Some(uuid),
1428 Some(1),
1429 Some(2),
1430 Action::PaymentFailed,
1431 Some(payload),
1432 ));
1433
1434 assert!(message.verify());
1436
1437 let message_json = message.as_json().unwrap();
1439
1440 let deserialized_message = Message::from_json(&message_json).unwrap();
1442 assert!(deserialized_message.verify());
1443
1444 if let Message::Order(kind) = deserialized_message {
1446 if let Some(Payload::PaymentFailed(info)) = kind.payload {
1447 assert_eq!(info.payment_attempts, 3);
1448 assert_eq!(info.payment_retries_interval, 60);
1449 } else {
1450 panic!("Expected PaymentFailed payload");
1451 }
1452 } else {
1453 panic!("Expected Order message");
1454 }
1455 }
1456
1457 #[test]
1458 fn test_message_payload_signature() {
1459 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1460 let peer = Peer::new(
1461 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1462 None, );
1464 let payload = Payload::Peer(peer);
1465 let test_message = Message::Order(MessageKind::new(
1466 Some(uuid),
1467 Some(1),
1468 Some(2),
1469 Action::FiatSentOk,
1470 Some(payload),
1471 ));
1472 assert!(test_message.verify());
1473 let test_message_json = test_message.as_json().unwrap();
1474 let trade_keys =
1476 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1477 .unwrap();
1478 let sig = Message::sign(test_message_json.clone(), &trade_keys);
1479
1480 assert!(Message::verify_signature(
1481 test_message_json,
1482 trade_keys.public_key(),
1483 sig
1484 ));
1485 }
1486
1487 #[test]
1488 fn test_restore_session_message() {
1489 let restore_request_message = Message::Restore(MessageKind::new(
1491 None,
1492 None,
1493 None,
1494 Action::RestoreSession,
1495 None,
1496 ));
1497
1498 assert!(restore_request_message.verify());
1500 assert_eq!(
1501 restore_request_message.inner_action(),
1502 Some(Action::RestoreSession)
1503 );
1504
1505 let message_json = restore_request_message.as_json().unwrap();
1507 let deserialized_message = Message::from_json(&message_json).unwrap();
1508 assert!(deserialized_message.verify());
1509 assert_eq!(
1510 deserialized_message.inner_action(),
1511 Some(Action::RestoreSession)
1512 );
1513
1514 let restored_orders = vec![
1516 crate::message::RestoredOrdersInfo {
1517 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1518 trade_index: 1,
1519 status: "active".to_string(),
1520 counterparty_trade_pubkey: Some(
1521 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1522 ),
1523 },
1524 crate::message::RestoredOrdersInfo {
1525 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1526 trade_index: 2,
1527 status: "success".to_string(),
1528 counterparty_trade_pubkey: None,
1529 },
1530 ];
1531
1532 let restored_disputes = vec![
1533 crate::message::RestoredDisputesInfo {
1534 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1535 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1536 trade_index: 1,
1537 status: "initiated".to_string(),
1538 initiator: Some(crate::message::DisputeInitiator::Buyer),
1539 solver_pubkey: None,
1540 },
1541 crate::message::RestoredDisputesInfo {
1542 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1543 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1544 trade_index: 2,
1545 status: "in-progress".to_string(),
1546 initiator: None,
1547 solver_pubkey: Some(
1548 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1549 ),
1550 },
1551 crate::message::RestoredDisputesInfo {
1552 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1553 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1554 trade_index: 3,
1555 status: "initiated".to_string(),
1556 initiator: Some(crate::message::DisputeInitiator::Seller),
1557 solver_pubkey: None,
1558 },
1559 ];
1560
1561 let restore_session_info = crate::message::RestoreSessionInfo {
1562 restore_orders: restored_orders.clone(),
1563 restore_disputes: restored_disputes.clone(),
1564 };
1565
1566 let restore_data_payload = Payload::RestoreData(restore_session_info);
1567 let restore_data_message = Message::Restore(MessageKind::new(
1568 None,
1569 None,
1570 None,
1571 Action::RestoreSession,
1572 Some(restore_data_payload),
1573 ));
1574
1575 assert!(!restore_data_message.verify());
1577
1578 let message_json = restore_data_message.as_json().unwrap();
1580 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1581
1582 if let Message::Restore(kind) = deserialized_restore_message {
1583 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1584 assert_eq!(session_info.restore_disputes.len(), 3);
1585 assert_eq!(
1586 session_info.restore_disputes[0].initiator,
1587 Some(crate::message::DisputeInitiator::Buyer)
1588 );
1589 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1590 assert_eq!(session_info.restore_disputes[1].initiator, None);
1591 assert_eq!(
1592 session_info.restore_disputes[1].solver_pubkey,
1593 Some(
1594 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1595 .to_string()
1596 )
1597 );
1598 assert_eq!(
1599 session_info.restore_disputes[2].initiator,
1600 Some(crate::message::DisputeInitiator::Seller)
1601 );
1602 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1603 assert_eq!(
1604 session_info.restore_orders[0].counterparty_trade_pubkey,
1605 Some(
1606 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1607 .to_string()
1608 )
1609 );
1610 assert!(session_info.restore_orders[1]
1611 .counterparty_trade_pubkey
1612 .is_none());
1613 } else {
1614 panic!("Expected RestoreData payload");
1615 }
1616 } else {
1617 panic!("Expected Restore message");
1618 }
1619 }
1620
1621 #[test]
1624 fn restored_order_without_counterparty_pubkey_still_parses() {
1625 let json = r#"{
1626 "order_id": "308e1272-d5f4-47e6-bd97-3504baea9c23",
1627 "trade_index": 1,
1628 "status": "active"
1629 }"#;
1630
1631 let order: crate::message::RestoredOrdersInfo = serde_json::from_str(json).unwrap();
1632
1633 assert_eq!(order.trade_index, 1);
1634 assert_eq!(order.status, "active");
1635 assert!(order.counterparty_trade_pubkey.is_none());
1636 }
1637
1638 #[test]
1639 fn test_restore_session_message_validation() {
1640 let restore_request_message = Message::Restore(MessageKind::new(
1642 None,
1643 None,
1644 None,
1645 Action::RestoreSession,
1646 None, ));
1648
1649 assert!(restore_request_message.verify());
1651
1652 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1654 let wrong_message = Message::Restore(MessageKind::new(
1655 None,
1656 None,
1657 None,
1658 Action::RestoreSession,
1659 Some(wrong_payload),
1660 ));
1661
1662 assert!(!wrong_message.verify());
1664
1665 let with_id = Message::Restore(MessageKind::new(
1667 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1668 None,
1669 None,
1670 Action::RestoreSession,
1671 None,
1672 ));
1673 assert!(with_id.verify());
1674
1675 let with_request_id = Message::Restore(MessageKind::new(
1676 None,
1677 Some(42),
1678 None,
1679 Action::RestoreSession,
1680 None,
1681 ));
1682 assert!(with_request_id.verify());
1683
1684 let with_trade_index = Message::Restore(MessageKind::new(
1685 None,
1686 None,
1687 Some(7),
1688 Action::RestoreSession,
1689 None,
1690 ));
1691 assert!(with_trade_index.verify());
1692 }
1693
1694 #[test]
1695 fn test_restore_session_message_constructor() {
1696 let restore_request_message = Message::new_restore(None);
1698
1699 assert!(matches!(restore_request_message, Message::Restore(_)));
1700 assert!(restore_request_message.verify());
1701 assert_eq!(
1702 restore_request_message.inner_action(),
1703 Some(Action::RestoreSession)
1704 );
1705
1706 let restore_session_info = crate::message::RestoreSessionInfo {
1708 restore_orders: vec![],
1709 restore_disputes: vec![],
1710 };
1711 let restore_data_message =
1712 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1713
1714 assert!(matches!(restore_data_message, Message::Restore(_)));
1715 assert!(!restore_data_message.verify());
1716 }
1717
1718 #[test]
1719 fn test_last_trade_index_valid_message() {
1720 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1721 let msg = Message::Restore(kind);
1722
1723 assert!(msg.verify());
1724
1725 let json = msg.as_json().unwrap();
1727 let decoded = Message::from_json(&json).unwrap();
1728 assert!(decoded.verify());
1729
1730 let inner = decoded.get_inner_message_kind();
1732 assert_eq!(inner.trade_index(), 7);
1733 assert_eq!(inner.has_trade_index(), (true, 7));
1734 }
1735
1736 #[test]
1737 fn test_last_trade_index_without_id_is_valid() {
1738 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1740 let msg = Message::Restore(kind);
1741 assert!(msg.verify());
1742 }
1743
1744 #[test]
1745 fn test_last_trade_index_with_payload_fails_validation() {
1746 let kind = MessageKind::new(
1748 None,
1749 None,
1750 Some(3),
1751 Action::LastTradeIndex,
1752 Some(Payload::TextMessage("ignored".to_string())),
1753 );
1754 let msg = Message::Restore(kind);
1755 assert!(!msg.verify());
1756 }
1757
1758 #[test]
1759 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1760 use crate::message::BondResolution;
1761
1762 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1763
1764 for action in [Action::AdminSettle, Action::AdminCancel] {
1765 let with_resolution = Message::Order(MessageKind::new(
1766 Some(uuid),
1767 Some(1),
1768 Some(2),
1769 action.clone(),
1770 Some(Payload::BondResolution(BondResolution {
1771 slash_seller: true,
1772 slash_buyer: false,
1773 })),
1774 ));
1775 assert!(
1776 with_resolution.verify(),
1777 "{action:?} + BondResolution should verify"
1778 );
1779
1780 let without_payload = Message::Order(MessageKind::new(
1781 Some(uuid),
1782 Some(1),
1783 Some(2),
1784 action.clone(),
1785 None,
1786 ));
1787 assert!(without_payload.verify(), "{action:?} + None should verify");
1788
1789 let wrong = Message::Order(MessageKind::new(
1791 Some(uuid),
1792 Some(1),
1793 Some(2),
1794 action.clone(),
1795 Some(Payload::TextMessage("nope".to_string())),
1796 ));
1797 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1798
1799 let no_id = Message::Order(MessageKind::new(
1801 None,
1802 Some(1),
1803 Some(2),
1804 action,
1805 Some(Payload::BondResolution(BondResolution {
1806 slash_seller: false,
1807 slash_buyer: false,
1808 })),
1809 ));
1810 assert!(!no_id.verify(), "admin action without id must be rejected");
1811 }
1812 }
1813
1814 #[test]
1815 fn test_bond_resolution_rejected_on_non_admin_actions() {
1816 use crate::message::BondResolution;
1817
1818 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1819 let payload = Payload::BondResolution(BondResolution {
1820 slash_seller: true,
1821 slash_buyer: true,
1822 });
1823
1824 for action in [
1828 Action::NewOrder,
1829 Action::TakeSell,
1830 Action::TakeBuy,
1831 Action::PayInvoice,
1832 Action::PayBondInvoice,
1833 Action::FiatSent,
1834 Action::FiatSentOk,
1835 Action::Release,
1836 Action::Released,
1837 Action::Cancel,
1838 Action::Canceled,
1839 Action::CooperativeCancelInitiatedByYou,
1840 Action::CooperativeCancelInitiatedByPeer,
1841 Action::DisputeInitiatedByYou,
1842 Action::DisputeInitiatedByPeer,
1843 Action::CooperativeCancelAccepted,
1844 Action::BuyerInvoiceAccepted,
1845 Action::BondInvoiceAccepted,
1846 Action::PurchaseCompleted,
1847 Action::BondPayoutCompleted,
1848 Action::BondSlashed,
1849 Action::HoldInvoicePaymentAccepted,
1850 Action::HoldInvoicePaymentSettled,
1851 Action::HoldInvoicePaymentCanceled,
1852 Action::WaitingSellerToPay,
1853 Action::WaitingBuyerInvoice,
1854 Action::AddInvoice,
1855 Action::AddBondInvoice,
1856 Action::BuyerTookOrder,
1857 Action::Rate,
1858 Action::RateUser,
1859 Action::RateReceived,
1860 Action::CantDo,
1861 Action::Dispute,
1862 Action::AdminCanceled,
1863 Action::AdminSettled,
1864 Action::AdminAddSolver,
1865 Action::AdminTakeDispute,
1866 Action::AdminTookDispute,
1867 Action::PaymentFailed,
1868 Action::InvoiceUpdated,
1869 Action::SendDm,
1870 Action::TradePubkey,
1871 Action::RestoreSession,
1872 Action::LastTradeIndex,
1873 Action::Orders,
1874 ] {
1875 let msg = Message::Order(MessageKind::new(
1876 Some(uuid),
1877 Some(1),
1878 Some(2),
1879 action.clone(),
1880 Some(payload.clone()),
1881 ));
1882 assert!(
1883 !msg.verify(),
1884 "{action:?} must reject BondResolution payload"
1885 );
1886 }
1887 }
1888
1889 #[test]
1890 fn test_bond_resolution_wire_format() {
1891 use crate::message::BondResolution;
1892
1893 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1894 let msg = Message::Order(MessageKind::new(
1895 Some(uuid),
1896 None,
1897 None,
1898 Action::AdminCancel,
1899 Some(Payload::BondResolution(BondResolution {
1900 slash_seller: true,
1901 slash_buyer: false,
1902 })),
1903 ));
1904
1905 let json = msg.as_json().unwrap();
1906 assert!(
1908 json.contains("\"bond_resolution\""),
1909 "expected snake_case discriminator, got: {json}"
1910 );
1911 assert!(json.contains("\"slash_seller\":true"));
1912 assert!(json.contains("\"slash_buyer\":false"));
1913
1914 let decoded = Message::from_json(&json).unwrap();
1916 assert!(decoded.verify());
1917 if let Message::Order(kind) = decoded {
1918 match kind.payload {
1919 Some(Payload::BondResolution(b)) => {
1920 assert!(b.slash_seller);
1921 assert!(!b.slash_buyer);
1922 }
1923 other => panic!("expected BondResolution payload, got {other:?}"),
1924 }
1925 } else {
1926 panic!("expected Order message");
1927 }
1928 }
1929
1930 #[test]
1931 fn test_bond_resolution_legacy_null_payload() {
1932 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1936 let json = format!(
1937 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
1938 );
1939 let msg = Message::from_json(&json).unwrap();
1940 assert!(msg.verify());
1941 }
1942
1943 #[test]
1944 fn test_pay_bond_invoice_wire_format_and_verify() {
1945 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1946 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1947
1948 let msg = Message::Order(MessageKind::new(
1949 Some(uuid),
1950 Some(1),
1951 Some(2),
1952 Action::PayBondInvoice,
1953 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1954 ));
1955 assert!(msg.verify());
1956
1957 let json = msg.as_json().unwrap();
1959 assert!(
1960 json.contains("\"action\":\"pay-bond-invoice\""),
1961 "expected kebab-case discriminator, got: {json}"
1962 );
1963
1964 let decoded = Message::from_json(&json).unwrap();
1966 assert!(decoded.verify());
1967 assert!(matches!(
1968 decoded.inner_action(),
1969 Some(Action::PayBondInvoice)
1970 ));
1971
1972 let no_id = Message::Order(MessageKind::new(
1974 None,
1975 Some(1),
1976 Some(2),
1977 Action::PayBondInvoice,
1978 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1979 ));
1980 assert!(!no_id.verify());
1981
1982 let wrong_payload = Message::Order(MessageKind::new(
1984 Some(uuid),
1985 Some(1),
1986 Some(2),
1987 Action::PayBondInvoice,
1988 Some(Payload::TextMessage("nope".to_string())),
1989 ));
1990 assert!(!wrong_payload.verify());
1991
1992 let no_payload = Message::Order(MessageKind::new(
1994 Some(uuid),
1995 Some(1),
1996 Some(2),
1997 Action::PayBondInvoice,
1998 None,
1999 ));
2000 assert!(!no_payload.verify());
2001 }
2002
2003 #[test]
2004 fn test_add_bond_invoice_wire_format_and_verify() {
2005 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2006 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
2007
2008 let msg = Message::Order(MessageKind::new(
2009 Some(uuid),
2010 Some(1),
2011 Some(2),
2012 Action::AddBondInvoice,
2013 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2014 ));
2015 assert!(msg.verify());
2016
2017 let json = msg.as_json().unwrap();
2019 assert!(
2020 json.contains("\"action\":\"add-bond-invoice\""),
2021 "expected kebab-case discriminator, got: {json}"
2022 );
2023
2024 let decoded = Message::from_json(&json).unwrap();
2026 assert!(decoded.verify());
2027 assert!(matches!(
2028 decoded.inner_action(),
2029 Some(Action::AddBondInvoice)
2030 ));
2031
2032 let no_id = Message::Order(MessageKind::new(
2034 None,
2035 Some(1),
2036 Some(2),
2037 Action::AddBondInvoice,
2038 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
2039 ));
2040 assert!(!no_id.verify());
2041
2042 let wrong_payload = Message::Order(MessageKind::new(
2044 Some(uuid),
2045 Some(1),
2046 Some(2),
2047 Action::AddBondInvoice,
2048 Some(Payload::TextMessage("nope".to_string())),
2049 ));
2050 assert!(!wrong_payload.verify());
2051
2052 let no_payload = Message::Order(MessageKind::new(
2054 Some(uuid),
2055 Some(1),
2056 Some(2),
2057 Action::AddBondInvoice,
2058 None,
2059 ));
2060 assert!(!no_payload.verify());
2061
2062 if let Message::Order(kind) = &msg {
2064 assert_eq!(kind.get_payment_request(), Some(bolt11));
2065 } else {
2066 panic!("expected Message::Order");
2067 }
2068 }
2069
2070 #[test]
2071 fn test_restored_dispute_helper_serialization_roundtrip() {
2072 use crate::message::RestoredDisputeHelper;
2073
2074 let helper = RestoredDisputeHelper {
2075 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
2076 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
2077 dispute_status: "initiated".to_string(),
2078 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
2079 master_seller_pubkey: Some("npub1sellerkey".to_string()),
2080 trade_index_buyer: Some(1),
2081 trade_index_seller: Some(2),
2082 buyer_dispute: true,
2083 seller_dispute: false,
2084 solver_pubkey: None,
2085 };
2086
2087 let json = serde_json::to_string(&helper).unwrap();
2088 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
2089
2090 assert_eq!(deserialized.dispute_id, helper.dispute_id);
2091 assert_eq!(deserialized.order_id, helper.order_id);
2092 assert_eq!(deserialized.dispute_status, helper.dispute_status);
2093 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
2094 assert_eq!(
2095 deserialized.master_seller_pubkey,
2096 helper.master_seller_pubkey
2097 );
2098 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
2099 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
2100 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
2101 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
2102 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
2103
2104 let helper_seller_dispute = RestoredDisputeHelper {
2105 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
2106 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
2107 dispute_status: "in-progress".to_string(),
2108 master_buyer_pubkey: None,
2109 master_seller_pubkey: None,
2110 trade_index_buyer: None,
2111 trade_index_seller: None,
2112 buyer_dispute: false,
2113 seller_dispute: true,
2114 solver_pubkey: Some(
2115 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
2116 ),
2117 };
2118
2119 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
2120 let deserialized_seller: RestoredDisputeHelper =
2121 serde_json::from_str(&json_seller).unwrap();
2122
2123 assert_eq!(
2124 deserialized_seller.dispute_id,
2125 helper_seller_dispute.dispute_id
2126 );
2127 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
2128 assert_eq!(
2129 deserialized_seller.dispute_status,
2130 helper_seller_dispute.dispute_status
2131 );
2132 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
2133 assert_eq!(deserialized_seller.master_seller_pubkey, None);
2134 assert_eq!(deserialized_seller.trade_index_buyer, None);
2135 assert_eq!(deserialized_seller.trade_index_seller, None);
2136 assert!(!deserialized_seller.buyer_dispute);
2137 assert!(deserialized_seller.seller_dispute);
2138 assert_eq!(
2139 deserialized_seller.solver_pubkey,
2140 helper_seller_dispute.solver_pubkey
2141 );
2142 }
2143
2144 fn sample_lock_proof() -> CashuLockProof {
2145 CashuLockProof::new(
2146 "cashuAeyJ0b2tlbiI6dGVzdA".to_string(),
2147 "https://mint.example".to_string(),
2148 "02b_buyer".to_string(),
2149 "02s_seller".to_string(),
2150 "02m_mostro".to_string(),
2151 )
2152 }
2153
2154 #[test]
2155 fn test_cashu_lock_proof_json_round_trip() {
2156 let proof = sample_lock_proof();
2157 let json = proof.as_json().unwrap();
2158 let back = CashuLockProof::from_json(&json).unwrap();
2159 assert_eq!(back, proof);
2160 }
2161
2162 #[test]
2163 fn test_cashu_lock_proof_fee_token_round_trip() {
2164 let proof = sample_lock_proof().with_fee_token("cashuAfee".to_string());
2165 let json = proof.as_json().unwrap();
2166 assert!(json.contains("fee_token"));
2167 let back = CashuLockProof::from_json(&json).unwrap();
2168 assert_eq!(back, proof);
2169 assert_eq!(back.fee_token.as_deref(), Some("cashuAfee"));
2170 }
2171
2172 #[test]
2173 fn test_cashu_lock_proof_without_fee_token_is_omitted_and_defaults_to_none() {
2174 let proof = sample_lock_proof();
2177 assert_eq!(proof.fee_token, None);
2178 let json = proof.as_json().unwrap();
2179 assert!(!json.contains("fee_token"));
2180 let back = CashuLockProof::from_json(&json).unwrap();
2181 assert_eq!(back.fee_token, None);
2182 }
2183
2184 #[test]
2185 fn test_add_cashu_escrow_verifies_with_lock_proof() {
2186 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2187 let payload = Payload::CashuLockProof(sample_lock_proof());
2188 let kind = MessageKind::new(
2189 Some(order_id),
2190 None,
2191 None,
2192 Action::AddCashuEscrow,
2193 Some(payload),
2194 );
2195 assert!(
2196 kind.verify(),
2197 "CashuLockProof must verify on AddCashuEscrow"
2198 );
2199
2200 let json = Message::Order(kind).as_json().unwrap();
2203 assert!(json.contains("cashu_lock_proof"));
2204 assert!(Message::from_json(&json).unwrap().verify());
2205 }
2206
2207 #[test]
2208 fn test_add_cashu_escrow_requires_id_and_right_payload() {
2209 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2210
2211 let no_id = MessageKind::new(
2213 None,
2214 None,
2215 None,
2216 Action::AddCashuEscrow,
2217 Some(Payload::CashuLockProof(sample_lock_proof())),
2218 );
2219 assert!(
2220 !no_id.verify(),
2221 "AddCashuEscrow without id must be rejected"
2222 );
2223
2224 let wrong_payload = MessageKind::new(
2226 Some(order_id),
2227 None,
2228 None,
2229 Action::AddCashuEscrow,
2230 Some(Payload::TextMessage("not a lock proof".to_string())),
2231 );
2232 assert!(
2233 !wrong_payload.verify(),
2234 "AddCashuEscrow with non-lock-proof payload must be rejected"
2235 );
2236 }
2237
2238 #[test]
2239 fn test_cashu_pm_signature_verifies_with_signatures() {
2240 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2241 let kind = MessageKind::new(
2242 Some(order_id),
2243 None,
2244 None,
2245 Action::CashuPmSignature,
2246 Some(Payload::CashuSignatures(vec![
2247 CashuProofSignature::new("secret-0".to_string(), "deadbeef".to_string()),
2248 CashuProofSignature::new("secret-1".to_string(), "c0ffee".to_string()),
2249 ])),
2250 );
2251 assert!(
2252 kind.verify(),
2253 "CashuSignatures must verify on CashuPmSignature"
2254 );
2255
2256 let json = Message::Order(kind).as_json().unwrap();
2257 assert!(json.contains("cashu_signatures"));
2258 assert!(Message::from_json(&json).unwrap().verify());
2259
2260 let wrong = MessageKind::new(Some(order_id), None, None, Action::CashuPmSignature, None);
2262 assert!(
2263 !wrong.verify(),
2264 "CashuPmSignature without a signature payload must be rejected"
2265 );
2266
2267 let empty = MessageKind::new(
2270 Some(order_id),
2271 None,
2272 None,
2273 Action::CashuPmSignature,
2274 Some(Payload::CashuSignatures(vec![])),
2275 );
2276 assert!(
2277 !empty.verify(),
2278 "CashuPmSignature with an empty signature set must be rejected"
2279 );
2280 }
2281
2282 #[test]
2283 fn test_cashu_escrow_locked_is_informational() {
2284 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2285
2286 let ok = MessageKind::new(Some(order_id), None, None, Action::CashuEscrowLocked, None);
2288 assert!(ok.verify(), "CashuEscrowLocked with id must verify");
2289
2290 let no_id = MessageKind::new(None, None, None, Action::CashuEscrowLocked, None);
2292 assert!(
2293 !no_id.verify(),
2294 "CashuEscrowLocked without id must be rejected"
2295 );
2296 }
2297}