use crate::prelude::*;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::Hash;
use bitcoin::key::Secp256k1;
use bitcoin::secp256k1::Message as BitcoinMessage;
use nostr_sdk::prelude::*;
#[cfg(feature = "sqlx")]
use sqlx::FromRow;
#[cfg(feature = "sqlx")]
use sqlx_crud::SqlxCrud;
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Peer {
pub pubkey: String,
pub reputation: Option<UserInfo>,
}
impl Peer {
pub fn new(pubkey: String, reputation: Option<UserInfo>) -> Self {
Self { pubkey, reputation }
}
pub fn from_json(json: &str) -> Result<Self, ServiceError> {
serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
}
pub fn as_json(&self) -> Result<String, ServiceError> {
serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
}
}
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum Action {
NewOrder,
TakeSell,
TakeBuy,
PayInvoice,
FiatSent,
FiatSentOk,
Release,
Released,
Cancel,
Canceled,
CooperativeCancelInitiatedByYou,
CooperativeCancelInitiatedByPeer,
DisputeInitiatedByYou,
DisputeInitiatedByPeer,
CooperativeCancelAccepted,
BuyerInvoiceAccepted,
PurchaseCompleted,
HoldInvoicePaymentAccepted,
HoldInvoicePaymentSettled,
HoldInvoicePaymentCanceled,
WaitingSellerToPay,
WaitingBuyerInvoice,
AddInvoice,
BuyerTookOrder,
Rate,
RateUser,
RateReceived,
CantDo,
Dispute,
AdminCancel,
AdminCanceled,
AdminSettle,
AdminSettled,
AdminAddSolver,
AdminTakeDispute,
AdminTookDispute,
PaymentFailed,
InvoiceUpdated,
SendDm,
TradePubkey,
RestoreSession,
LastTradeIndex,
Orders,
}
impl fmt::Display for Action {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Message {
Order(MessageKind),
Dispute(MessageKind),
CantDo(MessageKind),
Rate(MessageKind),
Dm(MessageKind),
Restore(MessageKind),
}
impl Message {
pub fn new_order(
id: Option<Uuid>,
request_id: Option<u64>,
trade_index: Option<i64>,
action: Action,
payload: Option<Payload>,
) -> Self {
let kind = MessageKind::new(id, request_id, trade_index, action, payload);
Self::Order(kind)
}
pub fn new_dispute(
id: Option<Uuid>,
request_id: Option<u64>,
trade_index: Option<i64>,
action: Action,
payload: Option<Payload>,
) -> Self {
let kind = MessageKind::new(id, request_id, trade_index, action, payload);
Self::Dispute(kind)
}
pub fn new_restore(payload: Option<Payload>) -> Self {
let kind = MessageKind::new(None, None, None, Action::RestoreSession, payload);
Self::Restore(kind)
}
pub fn cant_do(id: Option<Uuid>, request_id: Option<u64>, payload: Option<Payload>) -> Self {
let kind = MessageKind::new(id, request_id, None, Action::CantDo, payload);
Self::CantDo(kind)
}
pub fn new_dm(
id: Option<Uuid>,
request_id: Option<u64>,
action: Action,
payload: Option<Payload>,
) -> Self {
let kind = MessageKind::new(id, request_id, None, action, payload);
Self::Dm(kind)
}
pub fn from_json(json: &str) -> Result<Self, ServiceError> {
serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
}
pub fn as_json(&self) -> Result<String, ServiceError> {
serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
}
pub fn get_inner_message_kind(&self) -> &MessageKind {
match self {
Message::Dispute(k)
| Message::Order(k)
| Message::CantDo(k)
| Message::Rate(k)
| Message::Dm(k)
| Message::Restore(k) => k,
}
}
pub fn inner_action(&self) -> Option<Action> {
match self {
Message::Dispute(a)
| Message::Order(a)
| Message::CantDo(a)
| Message::Rate(a)
| Message::Dm(a)
| Message::Restore(a) => Some(a.get_action()),
}
}
pub fn verify(&self) -> bool {
match self {
Message::Order(m)
| Message::Dispute(m)
| Message::CantDo(m)
| Message::Rate(m)
| Message::Dm(m)
| Message::Restore(m) => m.verify(),
}
}
pub fn sign(message: String, keys: &Keys) -> Signature {
let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
let hash = hash.to_byte_array();
let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
keys.sign_schnorr(&message)
}
pub fn verify_signature(message: String, pubkey: PublicKey, sig: Signature) -> bool {
let hash: Sha256Hash = Sha256Hash::hash(message.as_bytes());
let hash = hash.to_byte_array();
let message: BitcoinMessage = BitcoinMessage::from_digest(hash);
let secp = Secp256k1::verification_only();
if let Ok(xonlykey) = pubkey.xonly() {
xonlykey.verify(&secp, &message, &sig).is_ok()
} else {
false
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MessageKind {
pub version: u8,
pub request_id: Option<u64>,
pub trade_index: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Uuid>,
pub action: Action,
pub payload: Option<Payload>,
}
type Amount = i64;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PaymentFailedInfo {
pub payment_attempts: u32,
pub payment_retries_interval: u32,
}
#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RestoredOrderHelper {
pub id: Uuid,
pub status: String,
pub master_buyer_pubkey: Option<String>,
pub master_seller_pubkey: Option<String>,
pub trade_index_buyer: Option<i64>,
pub trade_index_seller: Option<i64>,
}
#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RestoredDisputeHelper {
pub dispute_id: Uuid,
pub order_id: Uuid,
pub dispute_status: String,
pub master_buyer_pubkey: Option<String>,
pub master_seller_pubkey: Option<String>,
pub trade_index_buyer: Option<i64>,
pub trade_index_seller: Option<i64>,
pub buyer_dispute: bool,
pub seller_dispute: bool,
pub solver_pubkey: Option<String>,
}
#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RestoredOrdersInfo {
pub order_id: Uuid,
pub trade_index: i64,
pub status: String,
}
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "lowercase"))]
pub enum DisputeInitiator {
Buyer,
Seller,
}
#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud))]
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RestoredDisputesInfo {
pub dispute_id: Uuid,
pub order_id: Uuid,
pub trade_index: i64,
pub status: String,
pub initiator: Option<DisputeInitiator>,
pub solver_pubkey: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct RestoreSessionInfo {
#[serde(rename = "orders")]
pub restore_orders: Vec<RestoredOrdersInfo>,
#[serde(rename = "disputes")]
pub restore_disputes: Vec<RestoredDisputesInfo>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum Payload {
Order(SmallOrder),
PaymentRequest(Option<SmallOrder>, String, Option<Amount>),
TextMessage(String),
Peer(Peer),
RatingUser(u8),
Amount(Amount),
Dispute(Uuid, Option<SolverDisputeInfo>),
CantDo(Option<CantDoReason>),
NextTrade(String, u32),
PaymentFailed(PaymentFailedInfo),
RestoreData(RestoreSessionInfo),
Ids(Vec<Uuid>),
Orders(Vec<SmallOrder>),
}
#[allow(dead_code)]
impl MessageKind {
pub fn new(
id: Option<Uuid>,
request_id: Option<u64>,
trade_index: Option<i64>,
action: Action,
payload: Option<Payload>,
) -> Self {
Self {
version: PROTOCOL_VER,
request_id,
trade_index,
id,
action,
payload,
}
}
pub fn from_json(json: &str) -> Result<Self, ServiceError> {
serde_json::from_str(json).map_err(|_| ServiceError::MessageSerializationError)
}
pub fn as_json(&self) -> Result<String, ServiceError> {
serde_json::to_string(&self).map_err(|_| ServiceError::MessageSerializationError)
}
pub fn get_action(&self) -> Action {
self.action.clone()
}
pub fn get_next_trade_key(&self) -> Result<Option<(String, u32)>, ServiceError> {
match &self.payload {
Some(Payload::NextTrade(key, index)) => Ok(Some((key.to_string(), *index))),
None => Ok(None),
_ => Err(ServiceError::InvalidPayload),
}
}
pub fn get_rating(&self) -> Result<u8, ServiceError> {
if let Some(Payload::RatingUser(v)) = self.payload.to_owned() {
if !(MIN_RATING..=MAX_RATING).contains(&v) {
return Err(ServiceError::InvalidRatingValue);
}
Ok(v)
} else {
Err(ServiceError::InvalidRating)
}
}
pub fn verify(&self) -> bool {
match &self.action {
Action::NewOrder => matches!(&self.payload, Some(Payload::Order(_))),
Action::PayInvoice | Action::AddInvoice => {
if self.id.is_none() {
return false;
}
matches!(&self.payload, Some(Payload::PaymentRequest(_, _, _)))
}
Action::TakeSell
| Action::TakeBuy
| Action::FiatSent
| Action::FiatSentOk
| Action::Release
| Action::Released
| Action::Dispute
| Action::AdminCancel
| Action::AdminCanceled
| Action::AdminSettle
| Action::AdminSettled
| Action::Rate
| Action::RateReceived
| Action::AdminTakeDispute
| Action::AdminTookDispute
| Action::DisputeInitiatedByYou
| Action::DisputeInitiatedByPeer
| Action::WaitingBuyerInvoice
| Action::PurchaseCompleted
| Action::HoldInvoicePaymentAccepted
| Action::HoldInvoicePaymentSettled
| Action::HoldInvoicePaymentCanceled
| Action::WaitingSellerToPay
| Action::BuyerTookOrder
| Action::BuyerInvoiceAccepted
| Action::CooperativeCancelInitiatedByYou
| Action::CooperativeCancelInitiatedByPeer
| Action::CooperativeCancelAccepted
| Action::Cancel
| Action::InvoiceUpdated
| Action::AdminAddSolver
| Action::SendDm
| Action::TradePubkey
| Action::Canceled => {
if self.id.is_none() {
return false;
}
true
}
Action::LastTradeIndex | Action::RestoreSession => self.payload.is_none(),
Action::PaymentFailed => {
if self.id.is_none() {
return false;
}
matches!(&self.payload, Some(Payload::PaymentFailed(_)))
}
Action::RateUser => {
matches!(&self.payload, Some(Payload::RatingUser(_)))
}
Action::CantDo => {
matches!(&self.payload, Some(Payload::CantDo(_)))
}
Action::Orders => {
matches!(
&self.payload,
Some(Payload::Ids(_)) | Some(Payload::Orders(_))
)
}
}
}
pub fn get_order(&self) -> Option<&SmallOrder> {
if self.action != Action::NewOrder {
return None;
}
match &self.payload {
Some(Payload::Order(o)) => Some(o),
_ => None,
}
}
pub fn get_payment_request(&self) -> Option<String> {
if self.action != Action::TakeSell
&& self.action != Action::AddInvoice
&& self.action != Action::NewOrder
{
return None;
}
match &self.payload {
Some(Payload::PaymentRequest(_, pr, _)) => Some(pr.to_owned()),
Some(Payload::Order(ord)) => ord.buyer_invoice.to_owned(),
_ => None,
}
}
pub fn get_amount(&self) -> Option<Amount> {
if self.action != Action::TakeSell && self.action != Action::TakeBuy {
return None;
}
match &self.payload {
Some(Payload::PaymentRequest(_, _, amount)) => *amount,
Some(Payload::Amount(amount)) => Some(*amount),
_ => None,
}
}
pub fn get_payload(&self) -> Option<&Payload> {
self.payload.as_ref()
}
pub fn has_trade_index(&self) -> (bool, i64) {
if let Some(index) = self.trade_index {
return (true, index);
}
(false, 0)
}
pub fn trade_index(&self) -> i64 {
if let Some(index) = self.trade_index {
return index;
}
0
}
}
#[cfg(test)]
mod test {
use crate::message::{Action, Message, MessageKind, Payload, Peer};
use crate::user::UserInfo;
use nostr_sdk::Keys;
use uuid::uuid;
#[test]
fn test_peer_with_reputation() {
let reputation = UserInfo {
rating: 4.5,
reviews: 10,
operating_days: 30,
};
let peer = Peer::new(
"npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
Some(reputation.clone()),
);
assert_eq!(
peer.pubkey,
"npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
);
assert!(peer.reputation.is_some());
let peer_reputation = peer.reputation.clone().unwrap();
assert_eq!(peer_reputation.rating, 4.5);
assert_eq!(peer_reputation.reviews, 10);
assert_eq!(peer_reputation.operating_days, 30);
let json = peer.as_json().unwrap();
let deserialized_peer = Peer::from_json(&json).unwrap();
assert_eq!(deserialized_peer.pubkey, peer.pubkey);
assert!(deserialized_peer.reputation.is_some());
let deserialized_reputation = deserialized_peer.reputation.unwrap();
assert_eq!(deserialized_reputation.rating, 4.5);
assert_eq!(deserialized_reputation.reviews, 10);
assert_eq!(deserialized_reputation.operating_days, 30);
}
#[test]
fn test_peer_without_reputation() {
let peer = Peer::new(
"npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
None,
);
assert_eq!(
peer.pubkey,
"npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8"
);
assert!(peer.reputation.is_none());
let json = peer.as_json().unwrap();
let deserialized_peer = Peer::from_json(&json).unwrap();
assert_eq!(deserialized_peer.pubkey, peer.pubkey);
assert!(deserialized_peer.reputation.is_none());
}
#[test]
fn test_peer_in_message() {
let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
let reputation = UserInfo {
rating: 4.5,
reviews: 10,
operating_days: 30,
};
let peer_with_reputation = Peer::new(
"npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
Some(reputation),
);
let payload_with_reputation = Payload::Peer(peer_with_reputation);
let message_with_reputation = Message::Order(MessageKind::new(
Some(uuid),
Some(1),
Some(2),
Action::FiatSentOk,
Some(payload_with_reputation),
));
assert!(message_with_reputation.verify());
let message_json = message_with_reputation.as_json().unwrap();
let deserialized_message = Message::from_json(&message_json).unwrap();
assert!(deserialized_message.verify());
let peer_without_reputation = Peer::new(
"npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
None,
);
let payload_without_reputation = Payload::Peer(peer_without_reputation);
let message_without_reputation = Message::Order(MessageKind::new(
Some(uuid),
Some(1),
Some(2),
Action::FiatSentOk,
Some(payload_without_reputation),
));
assert!(message_without_reputation.verify());
let message_json = message_without_reputation.as_json().unwrap();
let deserialized_message = Message::from_json(&message_json).unwrap();
assert!(deserialized_message.verify());
}
#[test]
fn test_payment_failed_payload() {
let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
let payment_failed_info = crate::message::PaymentFailedInfo {
payment_attempts: 3,
payment_retries_interval: 60,
};
let payload = Payload::PaymentFailed(payment_failed_info);
let message = Message::Order(MessageKind::new(
Some(uuid),
Some(1),
Some(2),
Action::PaymentFailed,
Some(payload),
));
assert!(message.verify());
let message_json = message.as_json().unwrap();
let deserialized_message = Message::from_json(&message_json).unwrap();
assert!(deserialized_message.verify());
if let Message::Order(kind) = deserialized_message {
if let Some(Payload::PaymentFailed(info)) = kind.payload {
assert_eq!(info.payment_attempts, 3);
assert_eq!(info.payment_retries_interval, 60);
} else {
panic!("Expected PaymentFailed payload");
}
} else {
panic!("Expected Order message");
}
}
#[test]
fn test_message_payload_signature() {
let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
let peer = Peer::new(
"npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
None, );
let payload = Payload::Peer(peer);
let test_message = Message::Order(MessageKind::new(
Some(uuid),
Some(1),
Some(2),
Action::FiatSentOk,
Some(payload),
));
assert!(test_message.verify());
let test_message_json = test_message.as_json().unwrap();
let trade_keys =
Keys::parse("110e43647eae221ab1da33ddc17fd6ff423f2b2f49d809b9ffa40794a2ab996c")
.unwrap();
let sig = Message::sign(test_message_json.clone(), &trade_keys);
assert!(Message::verify_signature(
test_message_json,
trade_keys.public_key(),
sig
));
}
#[test]
fn test_restore_session_message() {
let restore_request_message = Message::Restore(MessageKind::new(
None,
None,
None,
Action::RestoreSession,
None,
));
assert!(restore_request_message.verify());
assert_eq!(
restore_request_message.inner_action(),
Some(Action::RestoreSession)
);
let message_json = restore_request_message.as_json().unwrap();
let deserialized_message = Message::from_json(&message_json).unwrap();
assert!(deserialized_message.verify());
assert_eq!(
deserialized_message.inner_action(),
Some(Action::RestoreSession)
);
let restored_orders = vec![
crate::message::RestoredOrdersInfo {
order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
trade_index: 1,
status: "active".to_string(),
},
crate::message::RestoredOrdersInfo {
order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
trade_index: 2,
status: "success".to_string(),
},
];
let restored_disputes = vec![
crate::message::RestoredDisputesInfo {
dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
trade_index: 1,
status: "initiated".to_string(),
initiator: Some(crate::message::DisputeInitiator::Buyer),
solver_pubkey: None,
},
crate::message::RestoredDisputesInfo {
dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
trade_index: 2,
status: "in-progress".to_string(),
initiator: None,
solver_pubkey: Some(
"aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
),
},
crate::message::RestoredDisputesInfo {
dispute_id: uuid!("708e1272-d5f4-47e6-bd97-3504baea9c27"),
order_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
trade_index: 3,
status: "initiated".to_string(),
initiator: Some(crate::message::DisputeInitiator::Seller),
solver_pubkey: None,
},
];
let restore_session_info = crate::message::RestoreSessionInfo {
restore_orders: restored_orders.clone(),
restore_disputes: restored_disputes.clone(),
};
let restore_data_payload = Payload::RestoreData(restore_session_info);
let restore_data_message = Message::Restore(MessageKind::new(
None,
None,
None,
Action::RestoreSession,
Some(restore_data_payload),
));
assert!(!restore_data_message.verify());
let message_json = restore_data_message.as_json().unwrap();
let deserialized_restore_message = Message::from_json(&message_json).unwrap();
if let Message::Restore(kind) = deserialized_restore_message {
if let Some(Payload::RestoreData(session_info)) = kind.payload {
assert_eq!(session_info.restore_disputes.len(), 3);
assert_eq!(
session_info.restore_disputes[0].initiator,
Some(crate::message::DisputeInitiator::Buyer)
);
assert!(session_info.restore_disputes[0].solver_pubkey.is_none());
assert_eq!(session_info.restore_disputes[1].initiator, None);
assert_eq!(
session_info.restore_disputes[1].solver_pubkey,
Some(
"aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
.to_string()
)
);
assert_eq!(
session_info.restore_disputes[2].initiator,
Some(crate::message::DisputeInitiator::Seller)
);
assert!(session_info.restore_disputes[2].solver_pubkey.is_none());
} else {
panic!("Expected RestoreData payload");
}
} else {
panic!("Expected Restore message");
}
}
#[test]
fn test_restore_session_message_validation() {
let restore_request_message = Message::Restore(MessageKind::new(
None,
None,
None,
Action::RestoreSession,
None, ));
assert!(restore_request_message.verify());
let wrong_payload = Payload::TextMessage("wrong payload".to_string());
let wrong_message = Message::Restore(MessageKind::new(
None,
None,
None,
Action::RestoreSession,
Some(wrong_payload),
));
assert!(!wrong_message.verify());
let with_id = Message::Restore(MessageKind::new(
Some(uuid!("00000000-0000-0000-0000-000000000001")),
None,
None,
Action::RestoreSession,
None,
));
assert!(with_id.verify());
let with_request_id = Message::Restore(MessageKind::new(
None,
Some(42),
None,
Action::RestoreSession,
None,
));
assert!(with_request_id.verify());
let with_trade_index = Message::Restore(MessageKind::new(
None,
None,
Some(7),
Action::RestoreSession,
None,
));
assert!(with_trade_index.verify());
}
#[test]
fn test_restore_session_message_constructor() {
let restore_request_message = Message::new_restore(None);
assert!(matches!(restore_request_message, Message::Restore(_)));
assert!(restore_request_message.verify());
assert_eq!(
restore_request_message.inner_action(),
Some(Action::RestoreSession)
);
let restore_session_info = crate::message::RestoreSessionInfo {
restore_orders: vec![],
restore_disputes: vec![],
};
let restore_data_message =
Message::new_restore(Some(Payload::RestoreData(restore_session_info)));
assert!(matches!(restore_data_message, Message::Restore(_)));
assert!(!restore_data_message.verify());
}
#[test]
fn test_last_trade_index_valid_message() {
let kind = MessageKind::new(None, None, Some(7), Action::LastTradeIndex, None);
let msg = Message::Restore(kind);
assert!(msg.verify());
let json = msg.as_json().unwrap();
let decoded = Message::from_json(&json).unwrap();
assert!(decoded.verify());
let inner = decoded.get_inner_message_kind();
assert_eq!(inner.trade_index(), 7);
assert_eq!(inner.has_trade_index(), (true, 7));
}
#[test]
fn test_last_trade_index_without_id_is_valid() {
let kind = MessageKind::new(None, None, Some(5), Action::LastTradeIndex, None);
let msg = Message::Restore(kind);
assert!(msg.verify());
}
#[test]
fn test_last_trade_index_with_payload_fails_validation() {
let kind = MessageKind::new(
None,
None,
Some(3),
Action::LastTradeIndex,
Some(Payload::TextMessage("ignored".to_string())),
);
let msg = Message::Restore(kind);
assert!(!msg.verify());
}
#[test]
fn test_restored_dispute_helper_serialization_roundtrip() {
use crate::message::RestoredDisputeHelper;
let helper = RestoredDisputeHelper {
dispute_id: uuid!("508e1272-d5f4-47e6-bd97-3504baea9c25"),
order_id: uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"),
dispute_status: "initiated".to_string(),
master_buyer_pubkey: Some("npub1buyerkey".to_string()),
master_seller_pubkey: Some("npub1sellerkey".to_string()),
trade_index_buyer: Some(1),
trade_index_seller: Some(2),
buyer_dispute: true,
seller_dispute: false,
solver_pubkey: None,
};
let json = serde_json::to_string(&helper).unwrap();
let deserialized: RestoredDisputeHelper = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.dispute_id, helper.dispute_id);
assert_eq!(deserialized.order_id, helper.order_id);
assert_eq!(deserialized.dispute_status, helper.dispute_status);
assert_eq!(deserialized.master_buyer_pubkey, helper.master_buyer_pubkey);
assert_eq!(
deserialized.master_seller_pubkey,
helper.master_seller_pubkey
);
assert_eq!(deserialized.trade_index_buyer, helper.trade_index_buyer);
assert_eq!(deserialized.trade_index_seller, helper.trade_index_seller);
assert_eq!(deserialized.buyer_dispute, helper.buyer_dispute);
assert_eq!(deserialized.seller_dispute, helper.seller_dispute);
assert_eq!(deserialized.solver_pubkey, helper.solver_pubkey);
let helper_seller_dispute = RestoredDisputeHelper {
dispute_id: uuid!("608e1272-d5f4-47e6-bd97-3504baea9c26"),
order_id: uuid!("408e1272-d5f4-47e6-bd97-3504baea9c24"),
dispute_status: "in-progress".to_string(),
master_buyer_pubkey: None,
master_seller_pubkey: None,
trade_index_buyer: None,
trade_index_seller: None,
buyer_dispute: false,
seller_dispute: true,
solver_pubkey: Some(
"aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344".to_string(),
),
};
let json_seller = serde_json::to_string(&helper_seller_dispute).unwrap();
let deserialized_seller: RestoredDisputeHelper =
serde_json::from_str(&json_seller).unwrap();
assert_eq!(
deserialized_seller.dispute_id,
helper_seller_dispute.dispute_id
);
assert_eq!(deserialized_seller.order_id, helper_seller_dispute.order_id);
assert_eq!(
deserialized_seller.dispute_status,
helper_seller_dispute.dispute_status
);
assert_eq!(deserialized_seller.master_buyer_pubkey, None);
assert_eq!(deserialized_seller.master_seller_pubkey, None);
assert_eq!(deserialized_seller.trade_index_buyer, None);
assert_eq!(deserialized_seller.trade_index_seller, None);
assert!(!deserialized_seller.buyer_dispute);
assert!(deserialized_seller.seller_dispute);
assert_eq!(
deserialized_seller.solver_pubkey,
helper_seller_dispute.solver_pubkey
);
}
}