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#[cfg(feature = "sqlx")]
22use sqlx_crud::SqlxCrud;
23
24use std::fmt;
25use uuid::Uuid;
26
27#[derive(Debug, Deserialize, Serialize, Clone)]
34pub struct Peer {
35 pub pubkey: String,
37 pub reputation: Option<UserInfo>,
40}
41
42impl Peer {
43 pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
45 Self { pubkey, reputation }
46 }
47
48 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
50 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
51 }
52
53 pub fn as_json(&self) -> Result<String, ServiceError> {
55 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
56 }
57}
58
59#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
65#[serde(rename_all = "kebab-case")]
66pub enum Action {
67 NewOrder,
69 TakeSell,
72 TakeBuy,
74 PayInvoice,
77 PayBondInvoice,
84 FiatSent,
86 FiatSentOk,
88 Release,
90 Released,
92 Cancel,
94 Canceled,
96 CooperativeCancelInitiatedByYou,
98 CooperativeCancelInitiatedByPeer,
100 DisputeInitiatedByYou,
102 DisputeInitiatedByPeer,
104 CooperativeCancelAccepted,
106 BuyerInvoiceAccepted,
108 BondInvoiceAccepted,
114 PurchaseCompleted,
116 BondPayoutCompleted,
122 BondSlashed,
130 HoldInvoicePaymentAccepted,
132 HoldInvoicePaymentSettled,
134 HoldInvoicePaymentCanceled,
136 WaitingSellerToPay,
138 WaitingBuyerInvoice,
140 AddInvoice,
143 AddBondInvoice,
150 BuyerTookOrder,
152 Rate,
154 RateUser,
156 RateReceived,
158 CantDo,
160 Dispute,
162 AdminCancel,
164 AdminCanceled,
166 AdminSettle,
168 AdminSettled,
170 AdminAddSolver,
172 AdminTakeDispute,
174 AdminTookDispute,
176 PaymentFailed,
179 InvoiceUpdated,
181 SendDm,
183 TradePubkey,
185 RestoreSession,
187 LastTradeIndex,
190 Orders,
193 AddCashuEscrow,
198 CashuEscrowLocked,
203 CashuPmSignature,
209}
210
211impl fmt::Display for Action {
212 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
213 write!(f, "{self:?}")
214 }
215}
216
217#[derive(Debug, Clone, Deserialize, Serialize)]
224#[serde(rename_all = "kebab-case")]
225pub enum Message {
226 Order(MessageKind),
228 Dispute(MessageKind),
230 CantDo(MessageKind),
232 Rate(MessageKind),
234 Dm(MessageKind),
236 Restore(MessageKind),
238}
239
240impl Message {
241 pub fn new_order(
244 id: Option<Uuid>,
245 request_id: Option<u64>,
246 trade_index: Option<i64>,
247 action: Action,
248 payload: Option<Payload>,
249 ) -> Self {
250 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
251 Self::Order(kind)
252 }
253
254 pub fn new_dispute(
257 id: Option<Uuid>,
258 request_id: Option<u64>,
259 trade_index: Option<i64>,
260 action: Action,
261 payload: Option<Payload>,
262 ) -> Self {
263 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
264
265 Self::Dispute(kind)
266 }
267
268 pub fn new_restore(payload: Option<Payload>) -> Self {
273 let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
274 Self::Restore(kind)
275 }
276
277 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
280 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
281
282 Self::CantDo(kind)
283 }
284
285 pub fn new_dm(
287 id: Option<Uuid>,
288 request_id: Option<u64>,
289 action: Action,
290 payload: Option<Payload>,
291 ) -> Self {
292 let kind = MessageKind::new(id, request_id, None, action, payload);
293
294 Self::Dm(kind)
295 }
296
297 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
299 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
300 }
301
302 pub fn as_json(&self) -> Result<String, ServiceError> {
304 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
305 }
306
307 pub fn get_inner_message_kind(&self) -> &MessageKind {
309 match self {
310 Message::Dispute(k)
311 | Message::Order(k)
312 | Message::CantDo(k)
313 | Message::Rate(k)
314 | Message::Dm(k)
315 | Message::Restore(k) => k,
316 }
317 }
318
319 pub fn inner_action(&self) -> Option<Action> {
324 match self {
325 Message::Dispute(a)
326 | Message::Order(a)
327 | Message::CantDo(a)
328 | Message::Rate(a)
329 | Message::Dm(a)
330 | Message::Restore(a) => Some(a.get_action()),
331 }
332 }
333
334 pub fn verify(&self) -> bool {
337 match self {
338 Message::Order(m)
339 | Message::Dispute(m)
340 | Message::CantDo(m)
341 | Message::Rate(m)
342 | Message::Dm(m)
343 | Message::Restore(m) => m.verify(),
344 }
345 }
346
347 pub fn sign(message: String, keys: &Keys) -> Signature {
356 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
357 let hash = hash.to_byte_array();
358 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
359
360 keys.sign_schnorr(&message)
361 }
362
363 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
369 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
371 let hash = hash.to_byte_array();
372 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
373
374 let secp = Secp256k1::verification_only();
376 if let Ok(xonlykey) = pubkey.xonly() {
378 xonlykey.verify(&secp, &message, &sig).is_ok()
379 } else {
380 false
381 }
382 }
383}
384
385#[derive(Debug, Clone, Deserialize, Serialize)]
392pub struct MessageKind {
393 pub version: u8,
396 pub request_id: Option<u64>,
399 pub trade_index: Option<i64>,
402 #[serde(skip_serializing_if = "Option::is_none")]
405 pub id: Option<Uuid>,
406 pub action: Action,
408 pub payload: Option<Payload>,
411}
412
413type Amount = i64;
415
416#[derive(Debug, Deserialize, Serialize, Clone)]
421pub struct PaymentFailedInfo {
422 pub payment_attempts: u32,
424 pub payment_retries_interval: u32,
426}
427
428#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
433#[derive(Debug, Deserialize, Serialize, Clone)]
434pub struct RestoredOrderHelper {
435 pub id: Uuid,
437 pub status: String,
439 pub master_buyer_pubkey: Option<String>,
441 pub master_seller_pubkey: Option<String>,
443 pub trade_index_buyer: Option<i64>,
445 pub trade_index_seller: Option<i64>,
447}
448
449#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
454#[derive(Debug, Deserialize, Serialize, Clone)]
455pub struct RestoredDisputeHelper {
456 pub dispute_id: Uuid,
458 pub order_id: Uuid,
460 pub dispute_status: String,
462 pub master_buyer_pubkey: Option<String>,
464 pub master_seller_pubkey: Option<String>,
466 pub trade_index_buyer: Option<i64>,
468 pub trade_index_seller: Option<i64>,
470 pub buyer_dispute: bool,
474 pub seller_dispute: bool,
478 pub solver_pubkey: Option<String>,
481}
482
483#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
485#[derive(Debug, Deserialize, Serialize, Clone)]
486pub struct RestoredOrdersInfo {
487 pub order_id: Uuid,
489 pub trade_index: i64,
491 pub status: String,
493}
494
495#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
497#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
498#[serde(rename_all = "lowercase")]
499#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
500pub enum DisputeInitiator {
501 Buyer,
503 Seller,
505}
506
507#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
509#[derive(Debug, Deserialize, Serialize, Clone)]
510pub struct RestoredDisputesInfo {
511 pub dispute_id: Uuid,
513 pub order_id: Uuid,
515 pub trade_index: i64,
517 pub status: String,
519 pub initiator: Option<DisputeInitiator>,
522 pub solver_pubkey: Option<String>,
525}
526
527#[derive(Debug, Deserialize, Serialize, Clone, Default)]
532pub struct RestoreSessionInfo {
533 #[serde(rename = "orders")]
535 pub restore_orders: Vec<RestoredOrdersInfo>,
536 #[serde(rename = "disputes")]
538 pub restore_disputes: Vec<RestoredDisputesInfo>,
539}
540
541#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
549pub struct BondResolution {
550 pub slash_seller: bool,
552 pub slash_buyer: bool,
554}
555
556#[derive(Debug, Deserialize, Serialize, Clone)]
571pub struct BondPayoutRequest {
572 pub order: SmallOrder,
576 pub slashed_at: i64,
581}
582
583#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
592pub struct CashuLockProof {
593 pub token: String,
595 pub mint_url: String,
598 pub buyer_pubkey: String,
600 pub seller_pubkey: String,
602 pub mostro_pubkey: String,
605}
606
607impl CashuLockProof {
608 pub fn new(
610 token: String,
611 mint_url: String,
612 buyer_pubkey: String,
613 seller_pubkey: String,
614 mostro_pubkey: String,
615 ) -> Self {
616 Self {
617 token,
618 mint_url,
619 buyer_pubkey,
620 seller_pubkey,
621 mostro_pubkey,
622 }
623 }
624
625 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
627 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
628 }
629
630 pub fn as_json(&self) -> Result<String, ServiceError> {
632 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
633 }
634}
635
636#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
645pub struct CashuProofSignature {
646 pub secret: String,
650 pub signature: String,
653}
654
655impl CashuProofSignature {
656 pub fn new(secret: String, signature: String) -> Self {
658 Self { secret, signature }
659 }
660}
661
662#[derive(Debug, Deserialize, Serialize, Clone)]
668#[serde(rename_all = "snake_case")]
669pub enum Payload {
670 Order(SmallOrder),
672 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
680 TextMessage(String),
682 Peer(Peer),
684 RatingUser(u8),
686 Amount(Amount),
688 Dispute(Uuid, Option<SolverDisputeInfo>),
691 CantDo(Option<CantDoReason>),
693 NextTrade(String, u32),
696 PaymentFailed(PaymentFailedInfo),
698 RestoreData(RestoreSessionInfo),
700 Ids(Vec<Uuid>),
702 Orders(Vec<SmallOrder>),
704 BondResolution(BondResolution),
707 BondPayoutRequest(BondPayoutRequest),
713 CashuLockProof(CashuLockProof),
716 CashuSignatures(Vec<CashuProofSignature>),
722}
723
724#[allow(dead_code)]
725impl MessageKind {
726 pub fn new(
729 id: Option<Uuid>,
730 request_id: Option<u64>,
731 trade_index: Option<i64>,
732 action: Action,
733 payload: Option<Payload>,
734 ) -> Self {
735 Self {
736 version: PROTOCOL_VER,
737 request_id,
738 trade_index,
739 id,
740 action,
741 payload,
742 }
743 }
744 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
746 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
747 }
748 pub fn as_json(&self) -> Result<String, ServiceError> {
750 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
751 }
752
753 pub fn get_action(&self) -> Action {
755 self.action.clone()
756 }
757
758 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
765 match &self.payload {
766 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
767 None => Ok(None),
768 _ => Err(ServiceError::InvalidPayload),
769 }
770 }
771
772 pub fn get_rating(&self) -> Result<u8, ServiceError> {
780 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
781 if !(MIN_RATING..=MAX_RATING).contains(&v) {
782 return Err(ServiceError::InvalidRatingValue);
783 }
784 Ok(v)
785 } else {
786 Err(ServiceError::InvalidRating)
787 }
788 }
789
790 pub fn verify(&self) -> bool {
797 match &self.action {
798 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
799 Action::PayInvoice | Action::PayBondInvoice | Action::AddInvoice => {
800 if self.id.is_none() {
801 return false;
802 }
803 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
804 }
805 Action::AddBondInvoice => {
806 if self.id.is_none() {
807 return false;
808 }
809 matches!(
815 &self.payload,
816 Some(Payload::BondPayoutRequest(_)) | Some(Payload::PaymentRequest(_, _, _))
817 )
818 }
819 Action::AdminSettle | Action::AdminCancel => {
820 if self.id.is_none() {
821 return false;
822 }
823 matches!(&self.payload, None | Some(Payload::BondResolution(_)))
824 }
825 Action::AddCashuEscrow => {
826 if self.id.is_none() {
827 return false;
828 }
829 matches!(&self.payload, Some(Payload::CashuLockProof(_)))
830 }
831 Action::CashuPmSignature => {
832 if self.id.is_none() {
833 return false;
834 }
835 matches!(&self.payload, Some(Payload::CashuSignatures(sigs)) if !sigs.is_empty())
836 }
837 Action::TakeSell
838 | Action::TakeBuy
839 | Action::FiatSent
840 | Action::FiatSentOk
841 | Action::Release
842 | Action::Released
843 | Action::Dispute
844 | Action::AdminCanceled
845 | Action::AdminSettled
846 | Action::Rate
847 | Action::RateReceived
848 | Action::AdminTakeDispute
849 | Action::AdminTookDispute
850 | Action::DisputeInitiatedByYou
851 | Action::DisputeInitiatedByPeer
852 | Action::WaitingBuyerInvoice
853 | Action::PurchaseCompleted
854 | Action::BondPayoutCompleted
855 | Action::BondSlashed
856 | Action::HoldInvoicePaymentAccepted
857 | Action::HoldInvoicePaymentSettled
858 | Action::HoldInvoicePaymentCanceled
859 | Action::WaitingSellerToPay
860 | Action::BuyerTookOrder
861 | Action::BuyerInvoiceAccepted
862 | Action::BondInvoiceAccepted
863 | Action::CooperativeCancelInitiatedByYou
864 | Action::CooperativeCancelInitiatedByPeer
865 | Action::CooperativeCancelAccepted
866 | Action::Cancel
867 | Action::InvoiceUpdated
868 | Action::AdminAddSolver
869 | Action::SendDm
870 | Action::TradePubkey
871 | Action::CashuEscrowLocked
872 | Action::Canceled => {
873 if self.id.is_none() {
874 return false;
875 }
876 !matches!(
877 &self.payload,
878 Some(Payload::BondResolution(_)) | Some(Payload::BondPayoutRequest(_))
879 )
880 }
881 Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
882 Action::PaymentFailed => {
883 if self.id.is_none() {
884 return false;
885 }
886 matches!(&self.payload, Some(Payload::PaymentFailed(_)))
887 }
888 Action::RateUser => {
889 matches!(&self.payload, Some(Payload::RatingUser(_)))
890 }
891 Action::CantDo => {
892 matches!(&self.payload, Some(Payload::CantDo(_)))
893 }
894 Action::Orders => {
895 matches!(
896 &self.payload,
897 Some(Payload::Ids(_)) | Some(Payload::Orders(_))
898 )
899 }
900 }
901 }
902
903 pub fn get_order(&self) -> Option<&SmallOrder> {
908 if self.action != Action::NewOrder {
909 return None;
910 }
911 match &self.payload {
912 Some(Payload::Order(o)) => Some(o),
913 _ => None,
914 }
915 }
916
917 pub fn get_payment_request(&self) -> Option<String> {
924 if self.action != Action::TakeSell
925 && self.action != Action::AddInvoice
926 && self.action != Action::AddBondInvoice
927 && self.action != Action::NewOrder
928 {
929 return None;
930 }
931 match &self.payload {
932 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
933 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
934 _ => None,
935 }
936 }
937
938 pub fn get_amount(&self) -> Option<Amount> {
942 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
943 return None;
944 }
945 match &self.payload {
946 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
947 Some(Payload::Amount(amount)) => Some(*amount),
948 _ => None,
949 }
950 }
951
952 pub fn get_payload(&self) -> Option<&Payload> {
954 self.payload.as_ref()
955 }
956
957 pub fn has_trade_index(&self) -> (bool, i64) {
960 if let Some(index) = self.trade_index {
961 return (true, index);
962 }
963 (false, 0)
964 }
965
966 pub fn trade_index(&self) -> i64 {
968 if let Some(index) = self.trade_index {
969 return index;
970 }
971 0
972 }
973}
974
975#[cfg(test)]
976mod test {
977 use crate::message::{
978 Action, BondPayoutRequest, CashuLockProof, CashuProofSignature, Message, MessageKind,
979 Payload, Peer,
980 };
981 use crate::order::SmallOrder;
982 use crate::user::UserInfo;
983 use nostr_sdk::Keys;
984 use uuid::uuid;
985
986 #[test]
987 fn test_peer_with_reputation() {
988 let reputation = UserInfo {
990 rating: 4.5,
991 reviews: 10,
992 operating_days: 30,
993 };
994 let peer = Peer::new(
995 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
996 Some(reputation.clone()),
997 );
998
999 assert_eq!(
1001 peer.pubkey,
1002 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1003 );
1004 assert!(peer.reputation.is_some());
1005 let peer_reputation = peer.reputation.clone().unwrap();
1006 assert_eq!(peer_reputation.rating, 4.5);
1007 assert_eq!(peer_reputation.reviews, 10);
1008 assert_eq!(peer_reputation.operating_days, 30);
1009
1010 let json = peer.as_json().unwrap();
1012 let deserialized_peer = Peer::from_json(&json).unwrap();
1013 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1014 assert!(deserialized_peer.reputation.is_some());
1015 let deserialized_reputation = deserialized_peer.reputation.unwrap();
1016 assert_eq!(deserialized_reputation.rating, 4.5);
1017 assert_eq!(deserialized_reputation.reviews, 10);
1018 assert_eq!(deserialized_reputation.operating_days, 30);
1019 }
1020
1021 #[test]
1022 fn test_peer_without_reputation() {
1023 let peer = Peer::new(
1025 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1026 None,
1027 );
1028
1029 assert_eq!(
1031 peer.pubkey,
1032 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
1033 );
1034 assert!(peer.reputation.is_none());
1035
1036 let json = peer.as_json().unwrap();
1038 let deserialized_peer = Peer::from_json(&json).unwrap();
1039 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
1040 assert!(deserialized_peer.reputation.is_none());
1041 }
1042
1043 #[test]
1044 fn test_peer_in_message() {
1045 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1046
1047 let reputation = UserInfo {
1049 rating: 4.5,
1050 reviews: 10,
1051 operating_days: 30,
1052 };
1053 let peer_with_reputation = Peer::new(
1054 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1055 Some(reputation),
1056 );
1057 let payload_with_reputation = Payload::Peer(peer_with_reputation);
1058 let message_with_reputation = Message::Order(MessageKind::new(
1059 Some(uuid),
1060 Some(1),
1061 Some(2),
1062 Action::FiatSentOk,
1063 Some(payload_with_reputation),
1064 ));
1065
1066 assert!(message_with_reputation.verify());
1068 let message_json = message_with_reputation.as_json().unwrap();
1069 let deserialized_message = Message::from_json(&message_json).unwrap();
1070 assert!(deserialized_message.verify());
1071
1072 let peer_without_reputation = Peer::new(
1074 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1075 None,
1076 );
1077 let payload_without_reputation = Payload::Peer(peer_without_reputation);
1078 let message_without_reputation = Message::Order(MessageKind::new(
1079 Some(uuid),
1080 Some(1),
1081 Some(2),
1082 Action::FiatSentOk,
1083 Some(payload_without_reputation),
1084 ));
1085
1086 assert!(message_without_reputation.verify());
1088 let message_json = message_without_reputation.as_json().unwrap();
1089 let deserialized_message = Message::from_json(&message_json).unwrap();
1090 assert!(deserialized_message.verify());
1091 }
1092
1093 #[test]
1094 fn test_bond_payout_request_payload_verifies_on_add_bond_invoice() {
1095 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1096 let order = SmallOrder {
1097 id: Some(order_id),
1098 kind: None,
1099 status: None,
1100 amount: 500,
1101 fiat_code: "USD".to_string(),
1102 min_amount: None,
1103 max_amount: None,
1104 fiat_amount: 0,
1105 payment_method: "lightning".to_string(),
1106 premium: 0,
1107 buyer_trade_pubkey: None,
1108 seller_trade_pubkey: None,
1109 buyer_invoice: None,
1110 created_at: None,
1111 expires_at: None,
1112 };
1113 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1114 order,
1115 slashed_at: 1_734_000_000,
1116 });
1117 let kind = MessageKind::new(
1118 Some(order_id),
1119 None,
1120 None,
1121 Action::AddBondInvoice,
1122 Some(payload),
1123 );
1124 assert!(
1125 kind.verify(),
1126 "BondPayoutRequest must verify on AddBondInvoice"
1127 );
1128
1129 let m = Message::Order(kind);
1132 let json = m.as_json().unwrap();
1133 assert!(json.contains("bond_payout_request"));
1134 let back = Message::from_json(&json).unwrap();
1135 assert!(back.verify());
1136 }
1137
1138 #[test]
1139 fn test_bond_payout_request_payload_rejected_on_wrong_action() {
1140 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1143 let order = SmallOrder {
1144 id: Some(order_id),
1145 kind: None,
1146 status: None,
1147 amount: 500,
1148 fiat_code: "USD".to_string(),
1149 min_amount: None,
1150 max_amount: None,
1151 fiat_amount: 0,
1152 payment_method: "lightning".to_string(),
1153 premium: 0,
1154 buyer_trade_pubkey: None,
1155 seller_trade_pubkey: None,
1156 buyer_invoice: None,
1157 created_at: None,
1158 expires_at: None,
1159 };
1160
1161 let _exhaustive: fn(Action) = |a| match a {
1166 Action::AddBondInvoice => {}
1167 Action::NewOrder
1168 | Action::TakeSell
1169 | Action::TakeBuy
1170 | Action::PayInvoice
1171 | Action::PayBondInvoice
1172 | Action::FiatSent
1173 | Action::FiatSentOk
1174 | Action::Release
1175 | Action::Released
1176 | Action::Cancel
1177 | Action::Canceled
1178 | Action::CooperativeCancelInitiatedByYou
1179 | Action::CooperativeCancelInitiatedByPeer
1180 | Action::DisputeInitiatedByYou
1181 | Action::DisputeInitiatedByPeer
1182 | Action::CooperativeCancelAccepted
1183 | Action::BuyerInvoiceAccepted
1184 | Action::BondInvoiceAccepted
1185 | Action::PurchaseCompleted
1186 | Action::BondPayoutCompleted
1187 | Action::BondSlashed
1188 | Action::HoldInvoicePaymentAccepted
1189 | Action::HoldInvoicePaymentSettled
1190 | Action::HoldInvoicePaymentCanceled
1191 | Action::WaitingSellerToPay
1192 | Action::WaitingBuyerInvoice
1193 | Action::AddInvoice
1194 | Action::BuyerTookOrder
1195 | Action::Rate
1196 | Action::RateUser
1197 | Action::RateReceived
1198 | Action::CantDo
1199 | Action::Dispute
1200 | Action::AdminCancel
1201 | Action::AdminCanceled
1202 | Action::AdminSettle
1203 | Action::AdminSettled
1204 | Action::AdminAddSolver
1205 | Action::AdminTakeDispute
1206 | Action::AdminTookDispute
1207 | Action::PaymentFailed
1208 | Action::InvoiceUpdated
1209 | Action::SendDm
1210 | Action::TradePubkey
1211 | Action::RestoreSession
1212 | Action::LastTradeIndex
1213 | Action::AddCashuEscrow
1214 | Action::CashuEscrowLocked
1215 | Action::CashuPmSignature
1216 | Action::Orders => {}
1217 };
1218
1219 let other_actions: &[Action] = &[
1220 Action::NewOrder,
1221 Action::TakeSell,
1222 Action::TakeBuy,
1223 Action::PayInvoice,
1224 Action::PayBondInvoice,
1225 Action::FiatSent,
1226 Action::FiatSentOk,
1227 Action::Release,
1228 Action::Released,
1229 Action::Cancel,
1230 Action::Canceled,
1231 Action::CooperativeCancelInitiatedByYou,
1232 Action::CooperativeCancelInitiatedByPeer,
1233 Action::DisputeInitiatedByYou,
1234 Action::DisputeInitiatedByPeer,
1235 Action::CooperativeCancelAccepted,
1236 Action::BuyerInvoiceAccepted,
1237 Action::BondInvoiceAccepted,
1238 Action::PurchaseCompleted,
1239 Action::BondPayoutCompleted,
1240 Action::BondSlashed,
1241 Action::HoldInvoicePaymentAccepted,
1242 Action::HoldInvoicePaymentSettled,
1243 Action::HoldInvoicePaymentCanceled,
1244 Action::WaitingSellerToPay,
1245 Action::WaitingBuyerInvoice,
1246 Action::AddInvoice,
1247 Action::BuyerTookOrder,
1248 Action::Rate,
1249 Action::RateUser,
1250 Action::RateReceived,
1251 Action::CantDo,
1252 Action::Dispute,
1253 Action::AdminCancel,
1254 Action::AdminCanceled,
1255 Action::AdminSettle,
1256 Action::AdminSettled,
1257 Action::AdminAddSolver,
1258 Action::AdminTakeDispute,
1259 Action::AdminTookDispute,
1260 Action::PaymentFailed,
1261 Action::InvoiceUpdated,
1262 Action::SendDm,
1263 Action::TradePubkey,
1264 Action::RestoreSession,
1265 Action::LastTradeIndex,
1266 Action::Orders,
1267 Action::AddCashuEscrow,
1268 Action::CashuEscrowLocked,
1269 Action::CashuPmSignature,
1270 ];
1271
1272 for action in other_actions {
1273 let payload = Payload::BondPayoutRequest(BondPayoutRequest {
1274 order: order.clone(),
1275 slashed_at: 0,
1276 });
1277 let kind = MessageKind::new(Some(order_id), None, None, action.clone(), Some(payload));
1278 assert!(
1279 !kind.verify(),
1280 "BondPayoutRequest must be rejected on {action:?}"
1281 );
1282 }
1283 }
1284
1285 #[test]
1286 fn test_bond_payout_ack_actions_verify_and_wire_format() {
1287 use crate::message::BondResolution;
1288
1289 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1290
1291 let order = || SmallOrder {
1294 id: Some(order_id),
1295 kind: None,
1296 status: None,
1297 amount: 500,
1298 fiat_code: "USD".to_string(),
1299 min_amount: None,
1300 max_amount: None,
1301 fiat_amount: 0,
1302 payment_method: "lightning".to_string(),
1303 premium: 0,
1304 buyer_trade_pubkey: None,
1305 seller_trade_pubkey: None,
1306 buyer_invoice: None,
1307 created_at: None,
1308 expires_at: None,
1309 };
1310
1311 for (action, discriminator) in [
1317 (Action::BondInvoiceAccepted, "bond-invoice-accepted"),
1318 (Action::BondPayoutCompleted, "bond-payout-completed"),
1319 (Action::BondSlashed, "bond-slashed"),
1320 ] {
1321 let ok = Message::Order(MessageKind::new(
1323 Some(order_id),
1324 Some(1),
1325 Some(2),
1326 action.clone(),
1327 Some(Payload::Order(order())),
1328 ));
1329 assert!(ok.verify(), "{action:?} + Order should verify");
1330
1331 let no_id = Message::Order(MessageKind::new(
1333 None,
1334 Some(1),
1335 Some(2),
1336 action.clone(),
1337 Some(Payload::Order(order())),
1338 ));
1339 assert!(!no_id.verify(), "{action:?} without id must be rejected");
1340
1341 let with_resolution = Message::Order(MessageKind::new(
1343 Some(order_id),
1344 Some(1),
1345 Some(2),
1346 action.clone(),
1347 Some(Payload::BondResolution(BondResolution {
1348 slash_seller: true,
1349 slash_buyer: false,
1350 })),
1351 ));
1352 assert!(
1353 !with_resolution.verify(),
1354 "{action:?} + BondResolution must be rejected"
1355 );
1356
1357 let with_request = Message::Order(MessageKind::new(
1359 Some(order_id),
1360 Some(1),
1361 Some(2),
1362 action.clone(),
1363 Some(Payload::BondPayoutRequest(BondPayoutRequest {
1364 order: order(),
1365 slashed_at: 0,
1366 })),
1367 ));
1368 assert!(
1369 !with_request.verify(),
1370 "{action:?} + BondPayoutRequest must be rejected"
1371 );
1372
1373 let json = ok.as_json().unwrap();
1375 assert!(
1376 json.contains(&format!("\"action\":\"{discriminator}\"")),
1377 "expected kebab-case discriminator {discriminator}, got: {json}"
1378 );
1379 let decoded = Message::from_json(&json).unwrap();
1380 assert!(decoded.verify());
1381 assert_eq!(decoded.inner_action(), Some(action));
1382 }
1383 }
1384
1385 #[test]
1386 fn test_payment_failed_payload() {
1387 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1388
1389 let payment_failed_info = crate::message::PaymentFailedInfo {
1391 payment_attempts: 3,
1392 payment_retries_interval: 60,
1393 };
1394
1395 let payload = Payload::PaymentFailed(payment_failed_info);
1396 let message = Message::Order(MessageKind::new(
1397 Some(uuid),
1398 Some(1),
1399 Some(2),
1400 Action::PaymentFailed,
1401 Some(payload),
1402 ));
1403
1404 assert!(message.verify());
1406
1407 let message_json = message.as_json().unwrap();
1409
1410 let deserialized_message = Message::from_json(&message_json).unwrap();
1412 assert!(deserialized_message.verify());
1413
1414 if let Message::Order(kind) = deserialized_message {
1416 if let Some(Payload::PaymentFailed(info)) = kind.payload {
1417 assert_eq!(info.payment_attempts, 3);
1418 assert_eq!(info.payment_retries_interval, 60);
1419 } else {
1420 panic!("Expected PaymentFailed payload");
1421 }
1422 } else {
1423 panic!("Expected Order message");
1424 }
1425 }
1426
1427 #[test]
1428 fn test_message_payload_signature() {
1429 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1430 let peer = Peer::new(
1431 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
1432 None, );
1434 let payload = Payload::Peer(peer);
1435 let test_message = Message::Order(MessageKind::new(
1436 Some(uuid),
1437 Some(1),
1438 Some(2),
1439 Action::FiatSentOk,
1440 Some(payload),
1441 ));
1442 assert!(test_message.verify());
1443 let test_message_json = test_message.as_json().unwrap();
1444 let trade_keys =
1446 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
1447 .unwrap();
1448 let sig = Message::sign(test_message_json.clone(), &trade_keys);
1449
1450 assert!(Message::verify_signature(
1451 test_message_json,
1452 trade_keys.public_key(),
1453 sig
1454 ));
1455 }
1456
1457 #[test]
1458 fn test_restore_session_message() {
1459 let restore_request_message = Message::Restore(MessageKind::new(
1461 None,
1462 None,
1463 None,
1464 Action::RestoreSession,
1465 None,
1466 ));
1467
1468 assert!(restore_request_message.verify());
1470 assert_eq!(
1471 restore_request_message.inner_action(),
1472 Some(Action::RestoreSession)
1473 );
1474
1475 let message_json = restore_request_message.as_json().unwrap();
1477 let deserialized_message = Message::from_json(&message_json).unwrap();
1478 assert!(deserialized_message.verify());
1479 assert_eq!(
1480 deserialized_message.inner_action(),
1481 Some(Action::RestoreSession)
1482 );
1483
1484 let restored_orders = vec![
1486 crate::message::RestoredOrdersInfo {
1487 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1488 trade_index: 1,
1489 status: "active".to_string(),
1490 },
1491 crate::message::RestoredOrdersInfo {
1492 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1493 trade_index: 2,
1494 status: "success".to_string(),
1495 },
1496 ];
1497
1498 let restored_disputes = vec![
1499 crate::message::RestoredDisputesInfo {
1500 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1501 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
1502 trade_index: 1,
1503 status: "initiated".to_string(),
1504 initiator: Some(crate::message::DisputeInitiator::Buyer),
1505 solver_pubkey: None,
1506 },
1507 crate::message::RestoredDisputesInfo {
1508 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
1509 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
1510 trade_index: 2,
1511 status: "in-progress".to_string(),
1512 initiator: None,
1513 solver_pubkey: Some(
1514 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
1515 ),
1516 },
1517 crate::message::RestoredDisputesInfo {
1518 dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
1519 order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
1520 trade_index: 3,
1521 status: "initiated".to_string(),
1522 initiator: Some(crate::message::DisputeInitiator::Seller),
1523 solver_pubkey: None,
1524 },
1525 ];
1526
1527 let restore_session_info = crate::message::RestoreSessionInfo {
1528 restore_orders: restored_orders.clone(),
1529 restore_disputes: restored_disputes.clone(),
1530 };
1531
1532 let restore_data_payload = Payload::RestoreData(restore_session_info);
1533 let restore_data_message = Message::Restore(MessageKind::new(
1534 None,
1535 None,
1536 None,
1537 Action::RestoreSession,
1538 Some(restore_data_payload),
1539 ));
1540
1541 assert!(!restore_data_message.verify());
1543
1544 let message_json = restore_data_message.as_json().unwrap();
1546 let deserialized_restore_message = Message::from_json(&message_json).unwrap();
1547
1548 if let Message::Restore(kind) = deserialized_restore_message {
1549 if let Some(Payload::RestoreData(session_info)) = kind.payload {
1550 assert_eq!(session_info.restore_disputes.len(), 3);
1551 assert_eq!(
1552 session_info.restore_disputes[0].initiator,
1553 Some(crate::message::DisputeInitiator::Buyer)
1554 );
1555 assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
1556 assert_eq!(session_info.restore_disputes[1].initiator, None);
1557 assert_eq!(
1558 session_info.restore_disputes[1].solver_pubkey,
1559 Some(
1560 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
1561 .to_string()
1562 )
1563 );
1564 assert_eq!(
1565 session_info.restore_disputes[2].initiator,
1566 Some(crate::message::DisputeInitiator::Seller)
1567 );
1568 assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
1569 } else {
1570 panic!("Expected RestoreData payload");
1571 }
1572 } else {
1573 panic!("Expected Restore message");
1574 }
1575 }
1576
1577 #[test]
1578 fn test_restore_session_message_validation() {
1579 let restore_request_message = Message::Restore(MessageKind::new(
1581 None,
1582 None,
1583 None,
1584 Action::RestoreSession,
1585 None, ));
1587
1588 assert!(restore_request_message.verify());
1590
1591 let wrong_payload = Payload::TextMessage("wrong payload".to_string());
1593 let wrong_message = Message::Restore(MessageKind::new(
1594 None,
1595 None,
1596 None,
1597 Action::RestoreSession,
1598 Some(wrong_payload),
1599 ));
1600
1601 assert!(!wrong_message.verify());
1603
1604 let with_id = Message::Restore(MessageKind::new(
1606 Some(uuid!("00000000-0000-0000-0000-000000000001")),
1607 None,
1608 None,
1609 Action::RestoreSession,
1610 None,
1611 ));
1612 assert!(with_id.verify());
1613
1614 let with_request_id = Message::Restore(MessageKind::new(
1615 None,
1616 Some(42),
1617 None,
1618 Action::RestoreSession,
1619 None,
1620 ));
1621 assert!(with_request_id.verify());
1622
1623 let with_trade_index = Message::Restore(MessageKind::new(
1624 None,
1625 None,
1626 Some(7),
1627 Action::RestoreSession,
1628 None,
1629 ));
1630 assert!(with_trade_index.verify());
1631 }
1632
1633 #[test]
1634 fn test_restore_session_message_constructor() {
1635 let restore_request_message = Message::new_restore(None);
1637
1638 assert!(matches!(restore_request_message, Message::Restore(_)));
1639 assert!(restore_request_message.verify());
1640 assert_eq!(
1641 restore_request_message.inner_action(),
1642 Some(Action::RestoreSession)
1643 );
1644
1645 let restore_session_info = crate::message::RestoreSessionInfo {
1647 restore_orders: vec![],
1648 restore_disputes: vec![],
1649 };
1650 let restore_data_message =
1651 Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
1652
1653 assert!(matches!(restore_data_message, Message::Restore(_)));
1654 assert!(!restore_data_message.verify());
1655 }
1656
1657 #[test]
1658 fn test_last_trade_index_valid_message() {
1659 let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
1660 let msg = Message::Restore(kind);
1661
1662 assert!(msg.verify());
1663
1664 let json = msg.as_json().unwrap();
1666 let decoded = Message::from_json(&json).unwrap();
1667 assert!(decoded.verify());
1668
1669 let inner = decoded.get_inner_message_kind();
1671 assert_eq!(inner.trade_index(), 7);
1672 assert_eq!(inner.has_trade_index(), (true, 7));
1673 }
1674
1675 #[test]
1676 fn test_last_trade_index_without_id_is_valid() {
1677 let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
1679 let msg = Message::Restore(kind);
1680 assert!(msg.verify());
1681 }
1682
1683 #[test]
1684 fn test_last_trade_index_with_payload_fails_validation() {
1685 let kind = MessageKind::new(
1687 None,
1688 None,
1689 Some(3),
1690 Action::LastTradeIndex,
1691 Some(Payload::TextMessage("ignored".to_string())),
1692 );
1693 let msg = Message::Restore(kind);
1694 assert!(!msg.verify());
1695 }
1696
1697 #[test]
1698 fn test_bond_resolution_admin_actions_accept_payload_or_none() {
1699 use crate::message::BondResolution;
1700
1701 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1702
1703 for action in [Action::AdminSettle, Action::AdminCancel] {
1704 let with_resolution = Message::Order(MessageKind::new(
1705 Some(uuid),
1706 Some(1),
1707 Some(2),
1708 action.clone(),
1709 Some(Payload::BondResolution(BondResolution {
1710 slash_seller: true,
1711 slash_buyer: false,
1712 })),
1713 ));
1714 assert!(
1715 with_resolution.verify(),
1716 "{action:?} + BondResolution should verify"
1717 );
1718
1719 let without_payload = Message::Order(MessageKind::new(
1720 Some(uuid),
1721 Some(1),
1722 Some(2),
1723 action.clone(),
1724 None,
1725 ));
1726 assert!(without_payload.verify(), "{action:?} + None should verify");
1727
1728 let wrong = Message::Order(MessageKind::new(
1730 Some(uuid),
1731 Some(1),
1732 Some(2),
1733 action.clone(),
1734 Some(Payload::TextMessage("nope".to_string())),
1735 ));
1736 assert!(!wrong.verify(), "{action:?} + TextMessage must be rejected");
1737
1738 let no_id = Message::Order(MessageKind::new(
1740 None,
1741 Some(1),
1742 Some(2),
1743 action,
1744 Some(Payload::BondResolution(BondResolution {
1745 slash_seller: false,
1746 slash_buyer: false,
1747 })),
1748 ));
1749 assert!(!no_id.verify(), "admin action without id must be rejected");
1750 }
1751 }
1752
1753 #[test]
1754 fn test_bond_resolution_rejected_on_non_admin_actions() {
1755 use crate::message::BondResolution;
1756
1757 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1758 let payload = Payload::BondResolution(BondResolution {
1759 slash_seller: true,
1760 slash_buyer: true,
1761 });
1762
1763 for action in [
1767 Action::NewOrder,
1768 Action::TakeSell,
1769 Action::TakeBuy,
1770 Action::PayInvoice,
1771 Action::PayBondInvoice,
1772 Action::FiatSent,
1773 Action::FiatSentOk,
1774 Action::Release,
1775 Action::Released,
1776 Action::Cancel,
1777 Action::Canceled,
1778 Action::CooperativeCancelInitiatedByYou,
1779 Action::CooperativeCancelInitiatedByPeer,
1780 Action::DisputeInitiatedByYou,
1781 Action::DisputeInitiatedByPeer,
1782 Action::CooperativeCancelAccepted,
1783 Action::BuyerInvoiceAccepted,
1784 Action::BondInvoiceAccepted,
1785 Action::PurchaseCompleted,
1786 Action::BondPayoutCompleted,
1787 Action::BondSlashed,
1788 Action::HoldInvoicePaymentAccepted,
1789 Action::HoldInvoicePaymentSettled,
1790 Action::HoldInvoicePaymentCanceled,
1791 Action::WaitingSellerToPay,
1792 Action::WaitingBuyerInvoice,
1793 Action::AddInvoice,
1794 Action::AddBondInvoice,
1795 Action::BuyerTookOrder,
1796 Action::Rate,
1797 Action::RateUser,
1798 Action::RateReceived,
1799 Action::CantDo,
1800 Action::Dispute,
1801 Action::AdminCanceled,
1802 Action::AdminSettled,
1803 Action::AdminAddSolver,
1804 Action::AdminTakeDispute,
1805 Action::AdminTookDispute,
1806 Action::PaymentFailed,
1807 Action::InvoiceUpdated,
1808 Action::SendDm,
1809 Action::TradePubkey,
1810 Action::RestoreSession,
1811 Action::LastTradeIndex,
1812 Action::Orders,
1813 ] {
1814 let msg = Message::Order(MessageKind::new(
1815 Some(uuid),
1816 Some(1),
1817 Some(2),
1818 action.clone(),
1819 Some(payload.clone()),
1820 ));
1821 assert!(
1822 !msg.verify(),
1823 "{action:?} must reject BondResolution payload"
1824 );
1825 }
1826 }
1827
1828 #[test]
1829 fn test_bond_resolution_wire_format() {
1830 use crate::message::BondResolution;
1831
1832 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1833 let msg = Message::Order(MessageKind::new(
1834 Some(uuid),
1835 None,
1836 None,
1837 Action::AdminCancel,
1838 Some(Payload::BondResolution(BondResolution {
1839 slash_seller: true,
1840 slash_buyer: false,
1841 })),
1842 ));
1843
1844 let json = msg.as_json().unwrap();
1845 assert!(
1847 json.contains("\"bond_resolution\""),
1848 "expected snake_case discriminator, got: {json}"
1849 );
1850 assert!(json.contains("\"slash_seller\":true"));
1851 assert!(json.contains("\"slash_buyer\":false"));
1852
1853 let decoded = Message::from_json(&json).unwrap();
1855 assert!(decoded.verify());
1856 if let Message::Order(kind) = decoded {
1857 match kind.payload {
1858 Some(Payload::BondResolution(b)) => {
1859 assert!(b.slash_seller);
1860 assert!(!b.slash_buyer);
1861 }
1862 other => panic!("expected BondResolution payload, got {other:?}"),
1863 }
1864 } else {
1865 panic!("expected Order message");
1866 }
1867 }
1868
1869 #[test]
1870 fn test_bond_resolution_legacy_null_payload() {
1871 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1875 let json = format!(
1876 r#"{{"order":{{"version":1,"id":"{uuid}","action":"admin-cancel","payload":null}}}}"#
1877 );
1878 let msg = Message::from_json(&json).unwrap();
1879 assert!(msg.verify());
1880 }
1881
1882 #[test]
1883 fn test_pay_bond_invoice_wire_format_and_verify() {
1884 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1885 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1886
1887 let msg = Message::Order(MessageKind::new(
1888 Some(uuid),
1889 Some(1),
1890 Some(2),
1891 Action::PayBondInvoice,
1892 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1893 ));
1894 assert!(msg.verify());
1895
1896 let json = msg.as_json().unwrap();
1898 assert!(
1899 json.contains("\"action\":\"pay-bond-invoice\""),
1900 "expected kebab-case discriminator, got: {json}"
1901 );
1902
1903 let decoded = Message::from_json(&json).unwrap();
1905 assert!(decoded.verify());
1906 assert!(matches!(
1907 decoded.inner_action(),
1908 Some(Action::PayBondInvoice)
1909 ));
1910
1911 let no_id = Message::Order(MessageKind::new(
1913 None,
1914 Some(1),
1915 Some(2),
1916 Action::PayBondInvoice,
1917 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1918 ));
1919 assert!(!no_id.verify());
1920
1921 let wrong_payload = Message::Order(MessageKind::new(
1923 Some(uuid),
1924 Some(1),
1925 Some(2),
1926 Action::PayBondInvoice,
1927 Some(Payload::TextMessage("nope".to_string())),
1928 ));
1929 assert!(!wrong_payload.verify());
1930
1931 let no_payload = Message::Order(MessageKind::new(
1933 Some(uuid),
1934 Some(1),
1935 Some(2),
1936 Action::PayBondInvoice,
1937 None,
1938 ));
1939 assert!(!no_payload.verify());
1940 }
1941
1942 #[test]
1943 fn test_add_bond_invoice_wire_format_and_verify() {
1944 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
1945 let bolt11 = "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e".to_string();
1946
1947 let msg = Message::Order(MessageKind::new(
1948 Some(uuid),
1949 Some(1),
1950 Some(2),
1951 Action::AddBondInvoice,
1952 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1953 ));
1954 assert!(msg.verify());
1955
1956 let json = msg.as_json().unwrap();
1958 assert!(
1959 json.contains("\"action\":\"add-bond-invoice\""),
1960 "expected kebab-case discriminator, got: {json}"
1961 );
1962
1963 let decoded = Message::from_json(&json).unwrap();
1965 assert!(decoded.verify());
1966 assert!(matches!(
1967 decoded.inner_action(),
1968 Some(Action::AddBondInvoice)
1969 ));
1970
1971 let no_id = Message::Order(MessageKind::new(
1973 None,
1974 Some(1),
1975 Some(2),
1976 Action::AddBondInvoice,
1977 Some(Payload::PaymentRequest(None, bolt11.clone(), None)),
1978 ));
1979 assert!(!no_id.verify());
1980
1981 let wrong_payload = Message::Order(MessageKind::new(
1983 Some(uuid),
1984 Some(1),
1985 Some(2),
1986 Action::AddBondInvoice,
1987 Some(Payload::TextMessage("nope".to_string())),
1988 ));
1989 assert!(!wrong_payload.verify());
1990
1991 let no_payload = Message::Order(MessageKind::new(
1993 Some(uuid),
1994 Some(1),
1995 Some(2),
1996 Action::AddBondInvoice,
1997 None,
1998 ));
1999 assert!(!no_payload.verify());
2000
2001 if let Message::Order(kind) = &msg {
2003 assert_eq!(kind.get_payment_request(), Some(bolt11));
2004 } else {
2005 panic!("expected Message::Order");
2006 }
2007 }
2008
2009 #[test]
2010 fn test_restored_dispute_helper_serialization_roundtrip() {
2011 use crate::message::RestoredDisputeHelper;
2012
2013 let helper = RestoredDisputeHelper {
2014 dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
2015 order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
2016 dispute_status: "initiated".to_string(),
2017 master_buyer_pubkey: Some("npub1buyerkey".to_string()),
2018 master_seller_pubkey: Some("npub1sellerkey".to_string()),
2019 trade_index_buyer: Some(1),
2020 trade_index_seller: Some(2),
2021 buyer_dispute: true,
2022 seller_dispute: false,
2023 solver_pubkey: None,
2024 };
2025
2026 let json = serde_json::to_string(&helper).unwrap();
2027 let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
2028
2029 assert_eq!(deserialized.dispute_id, helper.dispute_id);
2030 assert_eq!(deserialized.order_id, helper.order_id);
2031 assert_eq!(deserialized.dispute_status, helper.dispute_status);
2032 assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
2033 assert_eq!(
2034 deserialized.master_seller_pubkey,
2035 helper.master_seller_pubkey
2036 );
2037 assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
2038 assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
2039 assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
2040 assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
2041 assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
2042
2043 let helper_seller_dispute = RestoredDisputeHelper {
2044 dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
2045 order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
2046 dispute_status: "in-progress".to_string(),
2047 master_buyer_pubkey: None,
2048 master_seller_pubkey: None,
2049 trade_index_buyer: None,
2050 trade_index_seller: None,
2051 buyer_dispute: false,
2052 seller_dispute: true,
2053 solver_pubkey: Some(
2054 "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
2055 ),
2056 };
2057
2058 let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
2059 let deserialized_seller: RestoredDisputeHelper =
2060 serde_json::from_str(&json_seller).unwrap();
2061
2062 assert_eq!(
2063 deserialized_seller.dispute_id,
2064 helper_seller_dispute.dispute_id
2065 );
2066 assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
2067 assert_eq!(
2068 deserialized_seller.dispute_status,
2069 helper_seller_dispute.dispute_status
2070 );
2071 assert_eq!(deserialized_seller.master_buyer_pubkey, None);
2072 assert_eq!(deserialized_seller.master_seller_pubkey, None);
2073 assert_eq!(deserialized_seller.trade_index_buyer, None);
2074 assert_eq!(deserialized_seller.trade_index_seller, None);
2075 assert!(!deserialized_seller.buyer_dispute);
2076 assert!(deserialized_seller.seller_dispute);
2077 assert_eq!(
2078 deserialized_seller.solver_pubkey,
2079 helper_seller_dispute.solver_pubkey
2080 );
2081 }
2082
2083 fn sample_lock_proof() -> CashuLockProof {
2084 CashuLockProof::new(
2085 "cashuAeyJ0b2tlbiI6dGVzdA".to_string(),
2086 "https://mint.example".to_string(),
2087 "02b_buyer".to_string(),
2088 "02s_seller".to_string(),
2089 "02m_mostro".to_string(),
2090 )
2091 }
2092
2093 #[test]
2094 fn test_cashu_lock_proof_json_round_trip() {
2095 let proof = sample_lock_proof();
2096 let json = proof.as_json().unwrap();
2097 let back = CashuLockProof::from_json(&json).unwrap();
2098 assert_eq!(back, proof);
2099 }
2100
2101 #[test]
2102 fn test_add_cashu_escrow_verifies_with_lock_proof() {
2103 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2104 let payload = Payload::CashuLockProof(sample_lock_proof());
2105 let kind = MessageKind::new(
2106 Some(order_id),
2107 None,
2108 None,
2109 Action::AddCashuEscrow,
2110 Some(payload),
2111 );
2112 assert!(
2113 kind.verify(),
2114 "CashuLockProof must verify on AddCashuEscrow"
2115 );
2116
2117 let json = Message::Order(kind).as_json().unwrap();
2120 assert!(json.contains("cashu_lock_proof"));
2121 assert!(Message::from_json(&json).unwrap().verify());
2122 }
2123
2124 #[test]
2125 fn test_add_cashu_escrow_requires_id_and_right_payload() {
2126 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2127
2128 let no_id = MessageKind::new(
2130 None,
2131 None,
2132 None,
2133 Action::AddCashuEscrow,
2134 Some(Payload::CashuLockProof(sample_lock_proof())),
2135 );
2136 assert!(
2137 !no_id.verify(),
2138 "AddCashuEscrow without id must be rejected"
2139 );
2140
2141 let wrong_payload = MessageKind::new(
2143 Some(order_id),
2144 None,
2145 None,
2146 Action::AddCashuEscrow,
2147 Some(Payload::TextMessage("not a lock proof".to_string())),
2148 );
2149 assert!(
2150 !wrong_payload.verify(),
2151 "AddCashuEscrow with non-lock-proof payload must be rejected"
2152 );
2153 }
2154
2155 #[test]
2156 fn test_cashu_pm_signature_verifies_with_signatures() {
2157 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2158 let kind = MessageKind::new(
2159 Some(order_id),
2160 None,
2161 None,
2162 Action::CashuPmSignature,
2163 Some(Payload::CashuSignatures(vec![
2164 CashuProofSignature::new("secret-0".to_string(), "deadbeef".to_string()),
2165 CashuProofSignature::new("secret-1".to_string(), "c0ffee".to_string()),
2166 ])),
2167 );
2168 assert!(
2169 kind.verify(),
2170 "CashuSignatures must verify on CashuPmSignature"
2171 );
2172
2173 let json = Message::Order(kind).as_json().unwrap();
2174 assert!(json.contains("cashu_signatures"));
2175 assert!(Message::from_json(&json).unwrap().verify());
2176
2177 let wrong = MessageKind::new(Some(order_id), None, None, Action::CashuPmSignature, None);
2179 assert!(
2180 !wrong.verify(),
2181 "CashuPmSignature without a signature payload must be rejected"
2182 );
2183
2184 let empty = MessageKind::new(
2187 Some(order_id),
2188 None,
2189 None,
2190 Action::CashuPmSignature,
2191 Some(Payload::CashuSignatures(vec![])),
2192 );
2193 assert!(
2194 !empty.verify(),
2195 "CashuPmSignature with an empty signature set must be rejected"
2196 );
2197 }
2198
2199 #[test]
2200 fn test_cashu_escrow_locked_is_informational() {
2201 let order_id = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
2202
2203 let ok = MessageKind::new(Some(order_id), None, None, Action::CashuEscrowLocked, None);
2205 assert!(ok.verify(), "CashuEscrowLocked with id must verify");
2206
2207 let no_id = MessageKind::new(None, None, None, Action::CashuEscrowLocked, None);
2209 assert!(
2210 !no_id.verify(),
2211 "CashuEscrowLocked without id must be rejected"
2212 );
2213 }
2214}