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