diff --git a/src/app.rs b/src/app.rs index ad4730d7..b763d0eb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,17 +1,22 @@ -pub mod add_invoice; -pub mod admin_add_solver; -pub mod admin_cancel; -pub mod admin_settle; -pub mod admin_take_dispute; -pub mod cancel; -pub mod dispute; -pub mod fiat_sent; -pub mod order; -pub mod rate_user; -pub mod release; -pub mod take_buy; -pub mod take_sell; +//! Main application module for the P2P trading system. +//! Handles message routing, action processing, and event loop management. +// Submodules for different trading actions +pub mod add_invoice; // Handles invoice creation +pub mod admin_add_solver; // Admin functionality to add dispute solvers +pub mod admin_cancel; // Admin order cancellation +pub mod admin_settle; // Admin dispute settlement +pub mod admin_take_dispute; // Admin dispute handling +pub mod cancel; // User order cancellation +pub mod dispute; // User dispute handling +pub mod fiat_sent; // Fiat payment confirmation +pub mod order; // Order creation and management +pub mod rate_user; // User reputation system +pub mod release; // Release of held funds +pub mod take_buy; // Taking buy orders +pub mod take_sell; // Taking sell orders + +// Import action handlers from submodules use crate::app::add_invoice::add_invoice_action; use crate::app::admin_add_solver::admin_add_solver_action; use crate::app::admin_cancel::admin_cancel_action; @@ -25,23 +30,86 @@ use crate::app::rate_user::update_user_reputation_action; use crate::app::release::release_action; use crate::app::take_buy::take_buy_action; use crate::app::take_sell::take_sell_action; + +// Core functionality imports use crate::lightning::LndConnector; use crate::nip59::unwrap_gift_wrap; use crate::Settings; +// External dependencies use anyhow::Result; use mostro_core::message::{Action, Message}; use nostr_sdk::prelude::*; use sqlx::{Pool, Sqlite}; use std::sync::Arc; use tokio::sync::Mutex; -use tracing::error; -use tracing::info; +/// Helper function to log warning messages for action errors fn warning_msg(action: &Action, e: anyhow::Error) { tracing::warn!("Error in {} with context {}", action, e); } +/// Handles the processing of a single message action by routing it to the appropriate handler +/// based on the action type. This is the core message routing logic of the application. +/// +/// # Arguments +/// * `action` - The type of action to be performed +/// * `msg` - The message containing action details +/// * `event` - The unwrapped gift wrap event +/// * `my_keys` - Node keypair for signing/verification +/// * `pool` - Database connection pool +/// * `ln_client` - Lightning network connector +/// * `rate_list` - Shared list of rating events +async fn handle_message_action( + action: &Action, + msg: Message, + event: &UnwrappedGift, + my_keys: &Keys, + pool: &Pool, + ln_client: &mut LndConnector, + rate_list: Arc>>, +) -> Result<()> { + match action { + // Order-related actions + Action::NewOrder => order_action(msg, event, my_keys, pool).await, + Action::TakeSell => take_sell_action(msg, event, my_keys, pool).await, + Action::TakeBuy => take_buy_action(msg, event, my_keys, pool).await, + + // Payment-related actions + Action::FiatSent => fiat_sent_action(msg, event, my_keys, pool).await, + Action::Release => release_action(msg, event, my_keys, pool, ln_client).await, + Action::AddInvoice => add_invoice_action(msg, event, my_keys, pool).await, + Action::PayInvoice => todo!(), + + // Dispute and rating actions + Action::Dispute => dispute_action(msg, event, my_keys, pool).await, + Action::RateUser => { + update_user_reputation_action(msg, event, my_keys, pool, rate_list).await + } + Action::Cancel => cancel_action(msg, event, my_keys, pool, ln_client).await, + + // Admin actions + Action::AdminCancel => admin_cancel_action(msg, event, my_keys, pool, ln_client).await, + Action::AdminSettle => admin_settle_action(msg, event, my_keys, pool, ln_client).await, + Action::AdminAddSolver => admin_add_solver_action(msg, event, my_keys, pool).await, + Action::AdminTakeDispute => admin_take_dispute_action(msg, event, pool).await, + + _ => { + tracing::info!("Received message with action {:?}", action); + Ok(()) + } + } +} + +/// Main event loop that processes incoming Nostr events. +/// Handles message verification, POW checking, and routes valid messages to appropriate handlers. +/// +/// # Arguments +/// * `my_keys` - The node's keypair +/// * `client` - Nostr client instance +/// * `ln_client` - Lightning network connector +/// * `pool` - SQLite connection pool +/// * `rate_list` - Shared list of rating events pub async fn run( my_keys: Keys, client: &Client, @@ -56,20 +124,20 @@ pub async fn run( let pow = Settings::get_mostro().pow; while let Ok(notification) = notifications.recv().await { if let RelayPoolNotification::Event { event, .. } = notification { - // Verify pow + // Verify proof of work if !event.check_pow(pow) { - // Discard - info!("Not POW verified event!"); + // Discard events that don't meet POW requirements + tracing::info!("Not POW verified event!"); continue; } if let Kind::GiftWrap = event.kind { - // We validates if the event is correctly signed + // Validate event signature if event.verify().is_err() { tracing::warn!("Error in event verification") }; let event = unwrap_gift_wrap(&my_keys, &event)?; - // We discard events older than 10 seconds + // Discard events older than 10 seconds to prevent replay attacks let since_time = chrono::Utc::now() .checked_sub_signed(chrono::Duration::seconds(10)) .unwrap() @@ -78,127 +146,31 @@ pub async fn run( continue; } + // Parse and process the message let message = Message::from_json(&event.rumor.content); match message { Ok(msg) => { if msg.get_inner_message_kind().verify() { if let Some(action) = msg.inner_action() { - match action { - Action::NewOrder => { - if let Err(e) = - order_action(msg, &event, &my_keys, &pool).await - { - warning_msg(&action, e) - } - } - Action::TakeSell => { - if let Err(e) = - take_sell_action(msg, &event, &my_keys, &pool).await - { - warning_msg(&action, e) - } - } - Action::TakeBuy => { - if let Err(e) = - take_buy_action(msg, &event, &my_keys, &pool).await - { - warning_msg(&action, e) - } - } - Action::FiatSent => { - if let Err(e) = - fiat_sent_action(msg, &event, &my_keys, &pool).await - { - warning_msg(&action, e) - } - } - Action::Release => { - if let Err(e) = release_action( - msg, &event, &my_keys, &pool, ln_client, - ) - .await - { - warning_msg(&action, e) - } - } - Action::Cancel => { - if let Err(e) = cancel_action( - msg, &event, &my_keys, &pool, ln_client, - ) - .await - { - warning_msg(&action, e) - } - } - Action::AddInvoice => { - if let Err(e) = - add_invoice_action(msg, &event, &my_keys, &pool) - .await - { - warning_msg(&action, e) - } - } - Action::PayInvoice => todo!(), - Action::RateUser => { - if let Err(e) = update_user_reputation_action( - msg, - &event, - &my_keys, - &pool, - rate_list.clone(), - ) - .await - { - warning_msg(&action, e) - } - } - Action::Dispute => { - if let Err(e) = - dispute_action(msg, &event, &my_keys, &pool).await - { - warning_msg(&action, e) - } - } - Action::AdminCancel => { - if let Err(e) = admin_cancel_action( - msg, &event, &my_keys, &pool, ln_client, - ) - .await - { - warning_msg(&action, e) - } - } - Action::AdminSettle => { - if let Err(e) = admin_settle_action( - msg, &event, &my_keys, &pool, ln_client, - ) - .await - { - warning_msg(&action, e) - } - } - Action::AdminAddSolver => { - if let Err(e) = admin_add_solver_action( - msg, &event, &my_keys, &pool, - ) - .await - { - warning_msg(&action, e) - } - } - Action::AdminTakeDispute => { - if let Err(e) = - admin_take_dispute_action(msg, &event, &pool).await - { - warning_msg(&action, e) - } - } - _ => info!("Received message with action {:?}", action), + if let Err(e) = handle_message_action( + &action, + msg, + &event, + &my_keys, + &pool, + ln_client, + rate_list.clone(), + ) + .await + { + warning_msg(&action, e) } } } } - Err(e) => error!("Failed to parse message from JSON: {:?}", e), + Err(e) => { + tracing::warn!("Failed to parse event message from JSON: {:?}", e) + } } } } diff --git a/src/app/cancel.rs b/src/app/cancel.rs index c520d293..ab5dbc21 100644 --- a/src/app/cancel.rs +++ b/src/app/cancel.rs @@ -214,14 +214,7 @@ pub async fn cancel_add_invoice( &event.sender, ) .await; - send_new_order_msg( - None, - Some(order.id), - Action::Canceled, - None, - &seller_pubkey, - ) - .await; + send_new_order_msg(None, Some(order.id), Action::Canceled, None, &seller_pubkey).await; Ok(()) } else { // We re-publish the event with Pending status @@ -283,14 +276,7 @@ pub async fn cancel_pay_hold_invoice( &event.sender, ) .await; - send_new_order_msg( - None, - Some(order.id), - Action::Canceled, - None, - &seller_pubkey, - ) - .await; + send_new_order_msg(None, Some(order.id), Action::Canceled, None, &seller_pubkey).await; Ok(()) } else { // We re-publish the event with Pending status diff --git a/src/app/dispute.rs b/src/app/dispute.rs index 150e21cb..503a9917 100644 --- a/src/app/dispute.rs +++ b/src/app/dispute.rs @@ -1,3 +1,7 @@ +//! This module handles dispute-related functionality for the P2P trading system. +//! It provides mechanisms for users to initiate disputes, notify counterparties, +//! and publish dispute events to the network. + use std::borrow::Cow; use std::str::FromStr; @@ -14,8 +18,134 @@ use nostr_sdk::prelude::*; use rand::Rng; use sqlx::{Pool, Sqlite}; use sqlx_crud::traits::Crud; -use tracing::{error, info}; +use uuid::Uuid; + +/// Publishes a dispute event to the Nostr network. +/// +/// Creates and publishes a NIP-33 replaceable event containing dispute details +/// including status and application metadata. +async fn publish_dispute_event(dispute: &Dispute, my_keys: &Keys) -> Result<()> { + // Create tags for the dispute event + let tags = Tags::new(vec![ + // Status tag - indicates the current state of the dispute + Tag::custom( + TagKind::Custom(Cow::Borrowed("s")), + vec![dispute.status.to_string()], + ), + // Application identifier tag + Tag::custom( + TagKind::Custom(Cow::Borrowed("y")), + vec!["mostrop2p".to_string()], + ), + // Event type tag + Tag::custom( + TagKind::Custom(Cow::Borrowed("z")), + vec!["dispute".to_string()], + ), + ]); + + // Create a new NIP-33 replaceable event + // Empty content string as the information is in the tags + let event = new_event(my_keys, "", dispute.id.to_string(), tags) + .map_err(|_| Error::msg("Failed to create dispute event"))?; + + tracing::info!("Publishing dispute event: {:#?}", event); + + // Get nostr client and publish the event + match get_nostr_client() { + Ok(client) => match client.send_event(event).await { + Ok(_) => { + tracing::info!( + "Successfully published dispute event for dispute ID: {}", + dispute.id + ); + Ok(()) + } + Err(e) => { + tracing::error!("Failed to send dispute event: {}", e); + Err(Error::msg("Failed to send dispute event")) + } + }, + Err(e) => { + tracing::error!("Failed to get Nostr client: {}", e); + Err(Error::msg("Failed to get Nostr client")) + } + } +} + +/// Gets information about the counterparty in a dispute. +/// +/// Returns a tuple containing: +/// - The counterparty's public key as a String +/// - A boolean indicating if the dispute was initiated by the buyer (true) or seller (false) +fn get_counterpart_info(sender: &str, buyer: &str, seller: &str) -> Result<(String, bool)> { + match sender { + s if s == buyer => Ok((seller.to_string(), true)), // buyer is initiator + s if s == seller => Ok((buyer.to_string(), false)), // seller is initiator + _ => { + tracing::error!("Message sender {sender} is neither buyer nor seller"); + Err(Error::msg("Invalid message sender")) + } + } +} +/// Validates and retrieves an order from the database. +/// +/// Checks that: +/// - The order exists +/// - The order status allows disputes (Active or FiatSent) +async fn get_valid_order( + pool: &Pool, + order_id: Uuid, + event: &UnwrappedGift, + request_id: Option, +) -> Result { + // Try to fetch the order from the database + let order = match Order::by_id(pool, order_id).await? { + Some(order) => order, + None => { + tracing::error!("Order Id {order_id} not found!"); + return Err(Error::msg("Order not found")); + } + }; + + // Parse and validate the order status + match Status::from_str(&order.status) { + Ok(status) => { + // Only allow disputes for Active or FiatSent orders + if !matches!(status, Status::Active | Status::FiatSent) { + // Notify the sender that the action is not allowed for this status + send_new_order_msg( + request_id, + Some(order.id), + Action::NotAllowedByStatus, + None, + &event.sender, + ) + .await; + return Err(Error::msg(format!( + "Order {} with status {} does not allow disputes. Must be Active or FiatSent", + order.id, order.status + ))); + } + } + Err(_) => { + return Err(Error::msg("Invalid order status")); + } + }; + + Ok(order) +} + +/// Main handler for dispute actions. +/// +/// This function: +/// 1. Validates the order and dispute status +/// 2. Updates the order status +/// 3. Creates a new dispute record +/// 4. Generates security tokens for both parties +/// 5. Notifies both parties +/// 6. Publishes the dispute event to the network pub async fn dispute_action( msg: Message, event: &UnwrappedGift, @@ -33,36 +163,14 @@ pub async fn dispute_action( // Check dispute for this order id is yet present. if find_dispute_by_order_id(pool, order_id).await.is_ok() { - error!("Dispute yet opened for this order id: {order_id}"); - return Ok(()); + return Err(Error::msg(format!( + "Dispute already exists for order {}", + order_id + ))); } - let mut order = match Order::by_id(pool, order_id).await? { - Some(order) => { - if let Ok(st) = Status::from_str(&order.status) { - if matches!(st, Status::Active | Status::FiatSent) { - order - } else { - send_new_order_msg( - msg.get_inner_message_kind().request_id, - Some(order.id), - Action::NotAllowedByStatus, - None, - &event.sender, - ) - .await; - return Ok(()); - } - } else { - return Ok(()); - } - } - - None => { - error!("Order Id {order_id} not found!"); - return Ok(()); - } - }; + // Get and validate order + let mut order = get_valid_order(pool, order_id, event, request_id).await?; let (seller, buyer) = match (&order.seller_pubkey, &order.buyer_pubkey) { (Some(seller), Some(buyer)) => (seller.to_owned(), buyer.to_owned()), @@ -71,33 +179,25 @@ pub async fn dispute_action( }; let message_sender = event.sender.to_string(); - // Get counterpart pubkey - let mut counterpart: String = String::new(); - let mut buyer_dispute: bool = false; - let mut seller_dispute: bool = false; - - // Find the counterpart public key - if message_sender == buyer { - counterpart = seller; - buyer_dispute = true; - } else if message_sender == seller { - counterpart = buyer; - seller_dispute = true; - }; + let (counterpart, is_buyer_dispute) = + match get_counterpart_info(&message_sender, &buyer, &seller) { + Ok((counterpart, is_buyer_dispute)) => (counterpart, is_buyer_dispute), + Err(_) => { + send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + return Ok(()); + } + }; - // Add a check in case of no counterpart found - if counterpart.is_empty() { - // We create a Message - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; - return Ok(()); - }; + // Get the opposite dispute status + let is_seller_dispute = !is_buyer_dispute; + // Update dispute flags based on who initiated let mut update_seller_dispute = false; let mut update_buyer_dispute = false; - if seller_dispute && !order.seller_dispute { + if is_seller_dispute && !order.seller_dispute { update_seller_dispute = true; order.seller_dispute = update_seller_dispute; - } else if buyer_dispute && !order.buyer_dispute { + } else if is_buyer_dispute && !order.buyer_dispute { update_buyer_dispute = true; order.buyer_dispute = update_buyer_dispute; }; @@ -112,26 +212,26 @@ pub async fn dispute_action( order.update(pool).await?; } + // Create new dispute record and generate security tokens let mut dispute = Dispute::new(order_id); - // Generate tokens for the users to avoid fake resolver let mut rng = rand::thread_rng(); dispute.buyer_token = Some(rng.gen_range(100..=999)); dispute.seller_token = Some(rng.gen_range(100..=999)); - let (initiator_token, counterpart_token) = match seller_dispute { + let (initiator_token, counterpart_token) = match is_seller_dispute { true => (dispute.seller_token, dispute.buyer_token), false => (dispute.buyer_token, dispute.seller_token), }; - // Use CRUD create method + // Save dispute to database let dispute = dispute.create(pool).await?; - // We create a Message for the initiator + // Send notification to dispute initiator let initiator_pubkey = match PublicKey::from_str(&message_sender) { Ok(pk) => pk, Err(e) => { - error!("Error parsing initiator pubkey: {:#?}", e); - return Ok(()); + tracing::error!("Error parsing initiator pubkey: {:#?}", e); + return Err(Error::msg("Failed to parse initiator public key")); } }; @@ -144,12 +244,12 @@ pub async fn dispute_action( ) .await; - // We create a Message for the counterpart + // Send notification to counterparty let counterpart_pubkey = match PublicKey::from_str(&counterpart) { Ok(pk) => pk, Err(e) => { - error!("Error parsing counterpart pubkey: {:#?}", e); - return Ok(()); + tracing::error!("Error parsing counterpart pubkey: {:#?}", e); + return Err(Error::msg("Failed to parse counterpart public key")); } }; send_new_order_msg( @@ -161,29 +261,7 @@ pub async fn dispute_action( ) .await; - // We create a tag to show status of the dispute - let tags: Tags = Tags::new(vec![ - Tag::custom( - TagKind::Custom(Cow::Borrowed("s")), - vec![dispute.status.to_string()], - ), - Tag::custom( - TagKind::Custom(Cow::Borrowed("y")), - vec!["mostrop2p".to_string()], - ), - Tag::custom( - TagKind::Custom(Cow::Borrowed("z")), - vec!["dispute".to_string()], - ), - ]); - - // nip33 kind with dispute id as identifier - let event = new_event(my_keys, "", dispute.id.to_string(), tags)?; - info!("Dispute event to be published: {event:#?}"); - - if let Ok(client) = get_nostr_client() { - let _ = client.send_event(event).await; - } - + // Publish dispute event to network + publish_dispute_event(&dispute, my_keys).await?; Ok(()) }