1use crate::dispute::SolverDisputeInfo;
2use crate::error::ServiceError;
3use crate::user::UserInfo;
4use crate::{error::CantDoReason, order::SmallOrder};
5use bitcoin::hashes::sha256::Hash as Sha256Hash;
6use bitcoin::hashes::Hash;
7use bitcoin::key::Secp256k1;
8use bitcoin::secp256k1::Message as BitcoinMessage;
9use nostr_sdk::prelude::*;
10use serde::{Deserialize, Serialize};
11use std::fmt;
12use uuid::Uuid;
13
14pub const MAX_RATING: u8 = 5;
16pub const MIN_RATING: u8 = 1;
18
19pub const NOSTR_REPLACEABLE_EVENT_KIND: u16 = 38383;
22pub const PROTOCOL_VER: u8 = 1;
23
24#[derive(Debug, Deserialize, Serialize, Clone)]
26pub struct Peer {
27 pub pubkey: String,
28 pub reputation: Option<UserInfo>,
29}
30
31impl Peer {
32 pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
33 Self { pubkey, reputation }
34 }
35
36 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
37 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
38 }
39
40 pub fn as_json(&self) -> Result<String, ServiceError> {
41 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
42 }
43}
44
45#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
47#[serde(rename_all = "kebab-case")]
48pub enum Action {
49 NewOrder,
50 TakeSell,
51 TakeBuy,
52 PayInvoice,
53 FiatSent,
54 FiatSentOk,
55 Release,
56 Released,
57 Cancel,
58 Canceled,
59 CooperativeCancelInitiatedByYou,
60 CooperativeCancelInitiatedByPeer,
61 DisputeInitiatedByYou,
62 DisputeInitiatedByPeer,
63 CooperativeCancelAccepted,
64 BuyerInvoiceAccepted,
65 PurchaseCompleted,
66 HoldInvoicePaymentAccepted,
67 HoldInvoicePaymentSettled,
68 HoldInvoicePaymentCanceled,
69 WaitingSellerToPay,
70 WaitingBuyerInvoice,
71 AddInvoice,
72 BuyerTookOrder,
73 Rate,
74 RateUser,
75 RateReceived,
76 CantDo,
77 Dispute,
78 AdminCancel,
79 AdminCanceled,
80 AdminSettle,
81 AdminSettled,
82 AdminAddSolver,
83 AdminTakeDispute,
84 AdminTookDispute,
85 PaymentFailed,
86 InvoiceUpdated,
87 SendDm,
88 TradePubkey,
89}
90
91impl fmt::Display for Action {
92 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93 write!(f, "{self:?}")
94 }
95}
96
97#[derive(Debug, Clone, Deserialize, Serialize)]
99#[serde(rename_all = "kebab-case")]
100pub enum Message {
101 Order(MessageKind),
102 Dispute(MessageKind),
103 CantDo(MessageKind),
104 Rate(MessageKind),
105 Dm(MessageKind),
106}
107
108impl Message {
109 pub fn new_order(
111 id: Option<Uuid>,
112 request_id: Option<u64>,
113 trade_index: Option<i64>,
114 action: Action,
115 payload: Option<Payload>,
116 ) -> Self {
117 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
118 Self::Order(kind)
119 }
120
121 pub fn new_dispute(
123 id: Option<Uuid>,
124 request_id: Option<u64>,
125 trade_index: Option<i64>,
126 action: Action,
127 payload: Option<Payload>,
128 ) -> Self {
129 let kind = MessageKind::new(id, request_id, trade_index, action, payload);
130
131 Self::Dispute(kind)
132 }
133
134 pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
136 let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
137
138 Self::CantDo(kind)
139 }
140
141 pub fn new_dm(
143 id: Option<Uuid>,
144 request_id: Option<u64>,
145 action: Action,
146 payload: Option<Payload>,
147 ) -> Self {
148 let kind = MessageKind::new(id, request_id, None, action, payload);
149
150 Self::Dm(kind)
151 }
152
153 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
155 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
156 }
157
158 pub fn as_json(&self) -> Result<String, ServiceError> {
160 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
161 }
162
163 pub fn get_inner_message_kind(&self) -> &MessageKind {
165 match self {
166 Message::Dispute(k)
167 | Message::Order(k)
168 | Message::CantDo(k)
169 | Message::Rate(k)
170 | Message::Dm(k) => k,
171 }
172 }
173
174 pub fn inner_action(&self) -> Option<Action> {
176 match self {
177 Message::Dispute(a)
178 | Message::Order(a)
179 | Message::CantDo(a)
180 | Message::Rate(a)
181 | Message::Dm(a) => Some(a.get_action()),
182 }
183 }
184
185 pub fn verify(&self) -> bool {
187 match self {
188 Message::Order(m)
189 | Message::Dispute(m)
190 | Message::CantDo(m)
191 | Message::Rate(m)
192 | Message::Dm(m) => m.verify(),
193 }
194 }
195
196 pub fn sign(message: String, keys: &Keys) -> Signature {
197 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
198 let hash = hash.to_byte_array();
199 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
200
201 keys.sign_schnorr(&message)
202 }
203
204 pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
205 let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
207 let hash = hash.to_byte_array();
208 let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
209
210 let secp = Secp256k1::verification_only();
212 if let Ok(xonlykey) = pubkey.xonly() {
214 xonlykey.verify(&secp, &message, &sig).is_ok()
215 } else {
216 false
217 }
218 }
219}
220
221#[derive(Debug, Clone, Deserialize, Serialize)]
223pub struct MessageKind {
224 pub version: u8,
226 pub request_id: Option<u64>,
228 pub trade_index: Option<i64>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub id: Option<Uuid>,
233 pub action: Action,
235 pub payload: Option<Payload>,
237}
238
239type Amount = i64;
240
241#[derive(Debug, Deserialize, Serialize, Clone)]
243#[serde(rename_all = "snake_case")]
244pub enum Payload {
245 Order(SmallOrder),
247 PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
249 TextMessage(String),
251 Peer(Peer),
253 RatingUser(u8),
255 Amount(Amount),
257 Dispute(Uuid, Option<u16>, Option<SolverDisputeInfo>),
259 CantDo(Option<CantDoReason>),
261 NextTrade(String, u32),
265}
266
267#[allow(dead_code)]
268impl MessageKind {
269 pub fn new(
271 id: Option<Uuid>,
272 request_id: Option<u64>,
273 trade_index: Option<i64>,
274 action: Action,
275 payload: Option<Payload>,
276 ) -> Self {
277 Self {
278 version: PROTOCOL_VER,
279 request_id,
280 trade_index,
281 id,
282 action,
283 payload,
284 }
285 }
286 pub fn from_json(json: &str) -> Result<Self, ServiceError> {
288 serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
289 }
290 pub fn as_json(&self) -> Result<String, ServiceError> {
292 serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
293 }
294
295 pub fn get_action(&self) -> Action {
297 self.action.clone()
298 }
299
300 pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
302 match &self.payload {
303 Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
304 None => Ok(None),
305 _ => Err(ServiceError::InvalidPayload),
306 }
307 }
308
309 pub fn get_rating(&self) -> Result<u8, ServiceError> {
310 if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
311 if !(MIN_RATING..=MAX_RATING).contains(&v) {
312 return Err(ServiceError::InvalidRatingValue);
313 }
314 Ok(v)
315 } else {
316 Err(ServiceError::InvalidRating)
317 }
318 }
319
320 pub fn verify(&self) -> bool {
322 match &self.action {
323 Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
324 Action::PayInvoice | Action::AddInvoice => {
325 if self.id.is_none() {
326 return false;
327 }
328 matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
329 }
330 Action::TakeSell
331 | Action::TakeBuy
332 | Action::FiatSent
333 | Action::FiatSentOk
334 | Action::Release
335 | Action::Released
336 | Action::Dispute
337 | Action::AdminCancel
338 | Action::AdminCanceled
339 | Action::AdminSettle
340 | Action::AdminSettled
341 | Action::Rate
342 | Action::RateReceived
343 | Action::AdminTakeDispute
344 | Action::AdminTookDispute
345 | Action::DisputeInitiatedByYou
346 | Action::DisputeInitiatedByPeer
347 | Action::WaitingBuyerInvoice
348 | Action::PurchaseCompleted
349 | Action::HoldInvoicePaymentAccepted
350 | Action::HoldInvoicePaymentSettled
351 | Action::HoldInvoicePaymentCanceled
352 | Action::WaitingSellerToPay
353 | Action::BuyerTookOrder
354 | Action::BuyerInvoiceAccepted
355 | Action::CooperativeCancelInitiatedByYou
356 | Action::CooperativeCancelInitiatedByPeer
357 | Action::CooperativeCancelAccepted
358 | Action::Cancel
359 | Action::PaymentFailed
360 | Action::InvoiceUpdated
361 | Action::AdminAddSolver
362 | Action::SendDm
363 | Action::TradePubkey
364 | Action::Canceled => {
365 if self.id.is_none() {
366 return false;
367 }
368 true
369 }
370 Action::RateUser => {
371 matches!(&self.payload, Some(Payload::RatingUser(_)))
372 }
373 Action::CantDo => {
374 matches!(&self.payload, Some(Payload::CantDo(_)))
375 }
376 }
377 }
378
379 pub fn get_order(&self) -> Option<&SmallOrder> {
380 if self.action != Action::NewOrder {
381 return None;
382 }
383 match &self.payload {
384 Some(Payload::Order(o)) => Some(o),
385 _ => None,
386 }
387 }
388
389 pub fn get_payment_request(&self) -> Option<String> {
390 if self.action != Action::TakeSell
391 && self.action != Action::AddInvoice
392 && self.action != Action::NewOrder
393 {
394 return None;
395 }
396 match &self.payload {
397 Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
398 Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
399 _ => None,
400 }
401 }
402
403 pub fn get_amount(&self) -> Option<Amount> {
404 if self.action != Action::TakeSell && self.action != Action::TakeBuy {
405 return None;
406 }
407 match &self.payload {
408 Some(Payload::PaymentRequest(_, _, amount)) => *amount,
409 Some(Payload::Amount(amount)) => Some(*amount),
410 _ => None,
411 }
412 }
413
414 pub fn get_payload(&self) -> Option<&Payload> {
415 self.payload.as_ref()
416 }
417
418 pub fn has_trade_index(&self) -> (bool, i64) {
419 if let Some(index) = self.trade_index {
420 return (true, index);
421 }
422 (false, 0)
423 }
424
425 pub fn trade_index(&self) -> i64 {
426 if let Some(index) = self.trade_index {
427 return index;
428 }
429 0
430 }
431}
432
433#[cfg(test)]
434mod test {
435 use crate::message::{Action, Message, MessageKind, Payload, Peer};
436 use crate::user::UserInfo;
437 use nostr_sdk::Keys;
438 use uuid::uuid;
439
440 #[test]
441 fn test_peer_with_reputation() {
442 let reputation = UserInfo {
444 rating: 4.5,
445 reviews: 10,
446 operating_days: 30,
447 };
448 let peer = Peer::new(
449 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
450 Some(reputation.clone()),
451 );
452
453 assert_eq!(
455 peer.pubkey,
456 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
457 );
458 assert!(peer.reputation.is_some());
459 let peer_reputation = peer.reputation.clone().unwrap();
460 assert_eq!(peer_reputation.rating, 4.5);
461 assert_eq!(peer_reputation.reviews, 10);
462 assert_eq!(peer_reputation.operating_days, 30);
463
464 let json = peer.as_json().unwrap();
466 let deserialized_peer = Peer::from_json(&json).unwrap();
467 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
468 assert!(deserialized_peer.reputation.is_some());
469 let deserialized_reputation = deserialized_peer.reputation.unwrap();
470 assert_eq!(deserialized_reputation.rating, 4.5);
471 assert_eq!(deserialized_reputation.reviews, 10);
472 assert_eq!(deserialized_reputation.operating_days, 30);
473 }
474
475 #[test]
476 fn test_peer_without_reputation() {
477 let peer = Peer::new(
479 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
480 None,
481 );
482
483 assert_eq!(
485 peer.pubkey,
486 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
487 );
488 assert!(peer.reputation.is_none());
489
490 let json = peer.as_json().unwrap();
492 let deserialized_peer = Peer::from_json(&json).unwrap();
493 assert_eq!(deserialized_peer.pubkey, peer.pubkey);
494 assert!(deserialized_peer.reputation.is_none());
495 }
496
497 #[test]
498 fn test_peer_in_message() {
499 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
500
501 let reputation = UserInfo {
503 rating: 4.5,
504 reviews: 10,
505 operating_days: 30,
506 };
507 let peer_with_reputation = Peer::new(
508 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
509 Some(reputation),
510 );
511 let payload_with_reputation = Payload::Peer(peer_with_reputation);
512 let message_with_reputation = Message::Order(MessageKind::new(
513 Some(uuid),
514 Some(1),
515 Some(2),
516 Action::FiatSentOk,
517 Some(payload_with_reputation),
518 ));
519
520 assert!(message_with_reputation.verify());
522 let message_json = message_with_reputation.as_json().unwrap();
523 let deserialized_message = Message::from_json(&message_json).unwrap();
524 assert!(deserialized_message.verify());
525
526 let peer_without_reputation = Peer::new(
528 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
529 None,
530 );
531 let payload_without_reputation = Payload::Peer(peer_without_reputation);
532 let message_without_reputation = Message::Order(MessageKind::new(
533 Some(uuid),
534 Some(1),
535 Some(2),
536 Action::FiatSentOk,
537 Some(payload_without_reputation),
538 ));
539
540 assert!(message_without_reputation.verify());
542 let message_json = message_without_reputation.as_json().unwrap();
543 let deserialized_message = Message::from_json(&message_json).unwrap();
544 assert!(deserialized_message.verify());
545 }
546
547 #[test]
548 fn test_message_payload_signature() {
549 let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
550 let peer = Peer::new(
551 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
552 None, );
554 let payload = Payload::Peer(peer);
555 let test_message = Message::Order(MessageKind::new(
556 Some(uuid),
557 Some(1),
558 Some(2),
559 Action::FiatSentOk,
560 Some(payload),
561 ));
562 assert!(test_message.verify());
563 let test_message_json = test_message.as_json().unwrap();
564 let trade_keys =
566 Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
567 .unwrap();
568 let sig = Message::sign(test_message_json.clone(), &trade_keys);
569
570 assert!(Message::verify_signature(
571 test_message_json,
572 trade_keys.public_key(),
573 sig
574 ));
575 }
576}