From cf7728473684ad1b763f2a1eae0c30a77599c080 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Thu, 29 May 2025 23:49:21 +0200 Subject: [PATCH 01/19] feat: trying to improve messages rx without sleep of thread --- Cargo.lock | 4 +- src/cli.rs | 74 ++++-- src/cli/add_invoice.rs | 52 ++-- src/cli/get_dm.rs | 11 +- src/cli/list_disputes.rs | 5 +- src/cli/list_orders.rs | 13 +- src/cli/new_order.rs | 120 ++++------ src/cli/send_msg.rs | 147 ++++-------- src/cli/take_buy.rs | 113 +++------ src/cli/take_sell.rs | 115 ++++----- src/pretty_table.rs | 31 ++- src/util.rs | 505 +++++++++++++++++++++++++++++---------- 12 files changed, 688 insertions(+), 502 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7eaddce..541e1fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1950,9 +1950,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.4.2+3.4.1" +version = "300.5.2+3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" +checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" dependencies = [ "cc", ] diff --git a/src/cli.rs b/src/cli.rs index 5d7b70a..458f665 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -37,12 +37,20 @@ use crate::util; use anyhow::{Error, Result}; use clap::{Parser, Subcommand}; use nostr_sdk::prelude::*; +use std::sync::OnceLock; use std::{ env::{set_var, var}, str::FromStr, }; use take_dispute::*; use uuid::Uuid; +use sqlx::SqlitePool; + +pub static IDENTITY_KEYS: OnceLock = OnceLock::new(); +pub static MOSTRO_KEYS: OnceLock = OnceLock::new(); +pub static MOSTRO_PUBKEY: OnceLock = OnceLock::new(); +pub static POOL: OnceLock = OnceLock::new(); +pub static TRADE_KEY: OnceLock<(Keys, i64)> = OnceLock::new(); #[derive(Parser)] #[command( @@ -283,6 +291,31 @@ pub enum Commands { }, } +fn get_env_var(cli: &Cli) { + // Init logger + if cli.verbose { + set_var("RUST_LOG", "info"); + pretty_env_logger::init(); + } + + if cli.mostropubkey.is_some() { + set_var("MOSTRO_PUBKEY", cli.mostropubkey.unwrap()); + } + let pubkey = var("MOSTRO_PUBKEY").expect("$MOSTRO_PUBKEY env var needs to be set"); + + if cli.relays.is_some() { + set_var("RELAYS", cli.relays.unwrap()); + } + + if cli.pow.is_some() { + set_var("POW", cli.pow.unwrap()); + } + + if cli.secret { + set_var("SECRET", "true"); + } +} + // Check range with two values value fn check_fiat_range(s: &str) -> Result<(i64, Option)> { if s.contains('-') { @@ -326,40 +359,33 @@ fn check_fiat_range(s: &str) -> Result<(i64, Option)> { pub async fn run() -> Result<()> { let cli = Cli::parse(); - // Init logger - if cli.verbose { - set_var("RUST_LOG", "info"); - pretty_env_logger::init(); - } - - if cli.mostropubkey.is_some() { - set_var("MOSTRO_PUBKEY", cli.mostropubkey.unwrap()); - } - let pubkey = var("MOSTRO_PUBKEY").expect("$MOSTRO_PUBKEY env var needs to be set"); - - if cli.relays.is_some() { - set_var("RELAYS", cli.relays.unwrap()); - } - - if cli.pow.is_some() { - set_var("POW", cli.pow.unwrap()); - } - - if cli.secret { - set_var("SECRET", "true"); - } + // Get environment variables + get_env_var(&cli); + // Set pool as global variable to reuse in next calls let pool = connect().await?; + POOL.get_or_init(|| pool.clone()); + let identity_keys = User::get_identity_keys(&pool) .await .map_err(|e| anyhow::anyhow!("Failed to get identity keys: {}", e))?; + + // Set identity keys as global variable to reuse in next calls + IDENTITY_KEYS.get_or_init(|| identity_keys.clone()); + // Get next trade keys let (trade_keys, trade_index) = User::get_next_trade_keys(&pool) .await .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?; + // Set trade keys as global variable to reuse in next calls + TRADE_KEY.get_or_init(|| (trade_keys.clone(), trade_index)); + // Mostro pubkey - let mostro_key = PublicKey::from_str(&pubkey)?; + let mostro_key = Keys::from_str(&std::env::var("NSEC_PRIVKEY").map_err(|e| anyhow::anyhow!("Failed to get mostro keys: {}", e))?)?; + // Set mostro keys as global variable to reuse in next calls + MOSTRO_KEYS.get_or_init(|| mostro_key.clone()); + MOSTRO_PUBKEY.get_or_init(|| mostro_key.public_key()); // Call function to connect to relays let client = util::connect_nostr().await?; @@ -373,7 +399,7 @@ pub async fn run() -> Result<()> { status, currency, kind, - } => execute_list_orders(kind, currency, status, mostro_key, &client).await?, + } => execute_list_orders(kind, currency, status, &client).await?, Commands::TakeSell { order_id, invoice, diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index e9ee8ef..cbbe912 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -1,5 +1,5 @@ use crate::db::connect; -use crate::util::send_message_sync; +use crate::util::{send_dm, wait_for_dm}; use crate::{db::Order, lightning::is_valid_invoice}; use anyhow::Result; use lnurl::lightning_address::LightningAddress; @@ -16,7 +16,7 @@ pub async fn execute_add_invoice( client: &Client, ) -> Result<()> { let pool = connect().await?; - let mut order = Order::get_by_id(&pool, &order_id.to_string()).await?; + let order = Order::get_by_id(&pool, &order_id.to_string()).await?; let trade_keys = order .trade_keys .clone() @@ -50,31 +50,35 @@ pub async fn execute_add_invoice( payload, ); - let dm = send_message_sync( - client, - Some(identity_keys), - &trade_keys, - mostro_key, - add_invoice_message, - true, - false, - ) - .await?; + let message_json = add_invoice_message + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - dm.iter().for_each(|el| { - let message = el.0.get_inner_message_kind(); - if message.request_id == Some(request_id) && message.action == Action::WaitingSellerToPay { - println!("Now we should wait for the seller to pay the invoice"); + // Clone the keys and client for the async call + let identity_keys = identity_keys.clone(); + let trade_keys_clone = trade_keys.clone(); + let client_clone = client.clone(); + + // Spawn a new task to send the DM + // This is so we can wait for the gift wrap event in the main thread + tokio::spawn(async move { + if let Err(e) = send_dm( + &client_clone, + Some(&identity_keys.clone()), + &trade_keys_clone, + &mostro_key, + message_json, + None, + false, + ) + .await + { + eprintln!("Failed to send DM: {}", e); } }); - match order - .set_status(Status::WaitingPayment.to_string()) - .save(&pool) - .await - { - Ok(_) => println!("Order status updated"), - Err(e) => println!("Failed to update order status: {}", e), - } + + // Wait for the DM to be sent from mostro + wait_for_dm(client, &trade_keys, request_id, 0, Some(order)).await?; Ok(()) } diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index 9f46943..db7b631 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -2,6 +2,7 @@ use anyhow::Result; use chrono::DateTime; use mostro_core::prelude::*; use nostr_sdk::prelude::*; +use crate::cli::MOSTRO_KEYS; use crate::{ db::{connect, Order, User}, @@ -26,15 +27,7 @@ pub async fn execute_get_dm( dm.extend(dm_temp); } } else { - let id_key = match std::env::var("NSEC_PRIVKEY") { - Ok(id_key) => Keys::parse(&id_key)?, - Err(e) => { - println!("Failed to get mostro admin private key: {}", e); - std::process::exit(1); - } - }; - let dm_temp = - get_direct_messages(client, &id_key, *since, from_user, Some(mostro_pubkey)).await; + let dm_temp = get_direct_messages(client, MOSTRO_KEYS.get().unwrap(), *since, from_user).await; dm.extend(dm_temp); } diff --git a/src/cli/list_disputes.rs b/src/cli/list_disputes.rs index 6951647..0cc4cc5 100644 --- a/src/cli/list_disputes.rs +++ b/src/cli/list_disputes.rs @@ -2,7 +2,7 @@ use anyhow::Result; use nostr_sdk::prelude::*; use crate::pretty_table::print_disputes_table; -use crate::util::get_disputes_list; +use crate::util::{fetch_events_list, ListKind}; pub async fn execute_list_disputes(mostro_key: PublicKey, client: &Client) -> Result<()> { println!( @@ -11,7 +11,8 @@ pub async fn execute_list_disputes(mostro_key: PublicKey, client: &Client) -> Re ); // Get orders from relays - let table_of_disputes = get_disputes_list(mostro_key, client).await?; + let table_of_disputes = + fetch_events_list(mostro_key, ListKind::Disputes, None, None, None, client).await?; let table = print_disputes_table(table_of_disputes)?; println!("{table}"); diff --git a/src/cli/list_orders.rs b/src/cli/list_orders.rs index 6558437..f81eeaf 100644 --- a/src/cli/list_orders.rs +++ b/src/cli/list_orders.rs @@ -2,15 +2,14 @@ use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; use std::str::FromStr; - +use crate::cli::MOSTRO_PUBKEY; use crate::pretty_table::print_orders_table; -use crate::util::get_orders_list; +use crate::util::{fetch_events_list, ListKind}; pub async fn execute_list_orders( kind: &Option, currency: &Option, status: &Option, - mostro_key: PublicKey, client: &Client, ) -> Result<()> { // Used to get upper currency string to check against a list of tickers @@ -46,13 +45,13 @@ pub async fn execute_list_orders( println!( "Requesting orders from mostro pubId - {}", - mostro_key.clone() + MOSTRO_PUBKEY.get().unwrap() ); // Get orders from relays - let table_of_orders = get_orders_list( - mostro_key, - status_checked.unwrap(), + let table_of_orders = fetch_events_list( + ListKind::Orders, + status_checked, upper_currency, kind_checked, client, diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index 26af7b1..3ad51c2 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -7,9 +7,8 @@ use std::process; use std::str::FromStr; use uuid::Uuid; -use crate::db::{connect, Order, User}; use crate::pretty_table::print_order_preview; -use crate::util::{send_message_sync, uppercase_first}; +use crate::util::{send_dm, uppercase_first, wait_for_dm}; pub type FiatNames = HashMap; @@ -119,77 +118,50 @@ pub async fn execute_new_order( Some(order_content), ); - let dm = send_message_sync( - client, - Some(identity_keys), - trade_keys, - mostro_key, - message, - true, - false, - ) - .await?; - let order_id = dm - .iter() - .find_map(|el| { - let message = el.0.get_inner_message_kind(); - if message.request_id == Some(request_id) { - match message.action { - Action::NewOrder => { - if let Some(Payload::Order(order)) = message.payload.as_ref() { - return order.id; - } - } - Action::CantDo => { - if let Some(Payload::CantDo(Some(cant_do_reason))) = &message.payload { - match cant_do_reason { - CantDoReason::OutOfRangeFiatAmount | CantDoReason::OutOfRangeSatsAmount => { - println!("Error: Amount is outside the allowed range. Please check the order's min/max limits."); - } - _ => { - println!("Unknown reason: {:?}", message.payload); - } - } - } else { - println!("Unknown reason: {:?}", message.payload); - return None; - } - } - _ => { - println!("Unknown action: {:?}", message.action); - return None; - } - } - } - None - }) - .or_else(|| { - println!("Error: No matching order found in response"); - None - }); - - if let Some(order_id) = order_id { - println!("Order id {} created", order_id); - // Create order in db - let pool = connect().await?; - let db_order = Order::new(&pool, small_order, trade_keys, Some(request_id as i64)) - .await - .map_err(|e| anyhow::anyhow!("Failed to create DB order: {:?}", e))?; - // Update last trade index - match User::get(&pool).await { - Ok(mut user) => { - user.set_last_trade_index(trade_index); - if let Err(e) = user.save(&pool).await { - println!("Failed to update user: {}", e); - } - } - Err(e) => println!("Failed to get user: {}", e), - } - let db_order_id = db_order - .id - .clone() - .ok_or(anyhow::anyhow!("Missing order id"))?; - Order::save_new_id(&pool, db_order_id, order_id.to_string()).await?; - } + // Send dm to receiver pubkey + println!( + "SENDING DM with trade keys: {:?}", + trade_keys.public_key().to_hex() + ); + + // Serialize the message + let message_json = message + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; + + // Clone the keys and client for the async call + let identity_keys = identity_keys.clone(); + let trade_keys_clone = trade_keys.clone(); + // let mostro_key = mostro_key.clone(); + let client_clone = client.clone(); + + // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 + let subscription = Filter::new() + .pubkey(trade_keys.public_key()) + .kind(nostr_sdk::Kind::GiftWrap) + .limit(0); + + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); + + client.subscribe(subscription, Some(opts)).await?; + + // Spawn a new task to send the DM + // This is so we can wait for the gift wrap event in the main thread + tokio::spawn(async move { + let _ = send_dm( + &client_clone, + Some(&identity_keys.clone()), + &trade_keys_clone, + &mostro_key, + message_json, + None, + false, + ) + .await; + }); + + // Wait for the DM to be sent from mostro + wait_for_dm(client, trade_keys, request_id, trade_index, None).await?; + Ok(()) } diff --git a/src/cli/send_msg.rs b/src/cli/send_msg.rs index be6f2e3..8eaf05f 100644 --- a/src/cli/send_msg.rs +++ b/src/cli/send_msg.rs @@ -1,5 +1,5 @@ use crate::db::{Order, User}; -use crate::util::send_message_sync; +use crate::util::wait_for_dm; use crate::{cli::Commands, db::connect}; use anyhow::Result; @@ -34,7 +34,9 @@ pub async fn execute_send_msg( println!( "Sending {} command for order {:?} to mostro pubId {}", - requested_action, order_id, mostro_key + requested_action, + order_id.as_ref(), + mostro_key ); let pool = connect().await?; @@ -62,21 +64,55 @@ pub async fn execute_send_msg( // Create and send the message let message = Message::new_order(order_id, Some(request_id), None, requested_action, payload); - // println!("Sending message: {:#?}", message); + let client_clone = client.clone(); + let idkey = identity_keys + .ok_or_else(|| anyhow::anyhow!("Identity keys are required"))? + .to_owned(); if let Some(order_id) = order_id { - handle_order_response( - &pool, - client, - identity_keys, - mostro_key, - message, - order_id, - request_id, - ) - .await?; - } else { - println!("Error: Missing order ID"); + let order = Order::get_by_id(&pool, &order_id.to_string()).await?; + + if let Some(trade_keys_str) = order.trade_keys.clone() { + let trade_keys = Keys::parse(&trade_keys_str)?; + // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 + let subscription = Filter::new() + .pubkey(trade_keys.public_key()) + .kind(nostr_sdk::Kind::GiftWrap) + .limit(0); + + let opts = + SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); + + client.subscribe(subscription, Some(opts)).await?; + // Clone the keys and client for the async call + let trade_keys_clone = trade_keys.clone(); + + // Spawn a new task to send the DM + // This is so we can wait for the gift wrap event in the main thread + tokio::spawn(async move { + match message.as_json() { + Ok(message_json) => { + if let Err(e) = crate::util::send_dm( + &client_clone, + Some(&idkey), + &trade_keys_clone, + &mostro_key, + message_json, + None, + false, + ) + .await + { + eprintln!("Failed to send DM: {}", e); + } + } + Err(e) => eprintln!("Failed to serialize message: {}", e), + } + }); + + // Wait for the DM to be sent from mostro + wait_for_dm(client, &trade_keys, request_id, 0, Some(order)).await?; + } } Ok(()) @@ -103,84 +139,3 @@ async fn create_next_trade_payload( } Ok(None) } - -async fn handle_order_response( - pool: &SqlitePool, - client: &Client, - identity_keys: Option<&Keys>, - mostro_key: PublicKey, - message: Message, - order_id: Uuid, - request_id: u64, -) -> Result<()> { - let order = Order::get_by_id(pool, &order_id.to_string()).await; - - match order { - Ok(order) => { - if let Some(trade_keys_str) = order.trade_keys { - let trade_keys = Keys::parse(&trade_keys_str)?; - let dm = send_message_sync( - client, - identity_keys, - &trade_keys, - mostro_key, - message, - true, - false, - ) - .await?; - process_order_response(dm, pool, &trade_keys, request_id).await?; - } else { - println!("Error: Missing trade keys for order {}", order_id); - } - } - Err(e) => { - println!("Error: {}", e); - } - } - - Ok(()) -} - -async fn process_order_response( - dm: Vec<(Message, u64)>, - pool: &SqlitePool, - trade_keys: &Keys, - request_id: u64, -) -> Result<()> { - for (message, _) in dm { - let kind = message.get_inner_message_kind(); - if let Some(req_id) = kind.request_id { - if req_id != request_id { - continue; - } - - match kind.action { - Action::NewOrder => { - if let Some(Payload::Order(order)) = kind.payload.as_ref() { - Order::new(pool, order.clone(), trade_keys, Some(request_id as i64)) - .await - .map_err(|e| anyhow::anyhow!("Failed to create new order: {}", e))?; - return Ok(()); - } - } - Action::Canceled => { - if let Some(id) = kind.id { - // Verify order exists before deletion - if Order::get_by_id(pool, &id.to_string()).await.is_ok() { - Order::delete_by_id(pool, &id.to_string()) - .await - .map_err(|e| anyhow::anyhow!("Failed to delete order: {}", e))?; - return Ok(()); - } else { - return Err(anyhow::anyhow!("Order not found: {}", id)); - } - } - } - _ => (), - } - } - } - - Ok(()) -} diff --git a/src/cli/take_buy.rs b/src/cli/take_buy.rs index 55abc8a..883003c 100644 --- a/src/cli/take_buy.rs +++ b/src/cli/take_buy.rs @@ -3,10 +3,7 @@ use mostro_core::prelude::*; use nostr_sdk::prelude::*; use uuid::Uuid; -use crate::{ - db::{connect, Order, User}, - util::send_message_sync, -}; +use crate::util::{send_dm, wait_for_dm}; pub async fn execute_take_buy( order_id: &Uuid, @@ -33,79 +30,47 @@ pub async fn execute_take_buy( payload, ); - let dm = send_message_sync( - client, - Some(identity_keys), - trade_keys, - mostro_key, - take_buy_message, - true, - false, - ) - .await?; + // Send dm to receiver pubkey + println!( + "SENDING DM with trade keys: {:?}", + trade_keys.public_key().to_hex() + ); - let pool = connect().await?; + let message_json = take_buy_message + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - let order = dm.iter().find_map(|el| { - let message = el.0.get_inner_message_kind(); - if message.request_id == Some(request_id) { - match message.action { - Action::PayInvoice => { - if let Some(Payload::PaymentRequest(order, invoice, _)) = &message.payload { - println!( - "Mostro sent you this hold invoice for order id: {}", - order - .as_ref() - .and_then(|o| o.id) - .map_or("unknown".to_string(), |id| id.to_string()) - ); - println!(); - println!("Pay this invoice to continue --> {}", invoice); - println!(); - return order.clone(); - } - } - Action::CantDo => { - if let Some(Payload::CantDo(Some(cant_do_reason))) = &message.payload { - match cant_do_reason { - CantDoReason::OutOfRangeFiatAmount | CantDoReason::OutOfRangeSatsAmount => { - println!("Error: Amount is outside the allowed range. Please check the order's min/max limits."); - } - _ => { - println!("Unknown reason: {:?}", message.payload); - } - } - } else { - println!("Unknown reason: {:?}", message.payload); - return None; - } - } - _ => { - println!("Unknown action: {:?}", message.action); - return None; - } - } - } - None + // Clone the keys and client for the async call + let identity_keys = identity_keys.clone(); + let trade_keys_clone = trade_keys.clone(); + let client_clone = client.clone(); + // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 + let subscription = Filter::new() + .pubkey(trade_keys.public_key()) + .kind(nostr_sdk::Kind::GiftWrap) + .limit(0); + // Subscribe to gift wrap events -waiting for 1 event + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); + //Activate the subscription + client.subscribe(subscription, Some(opts)).await?; + + // Spawn a new task to send the DM + // This is so we can wait for the gift wrap event in the main thread + tokio::spawn(async move { + let _ = send_dm( + &client_clone, + Some(&identity_keys.clone()), + &trade_keys_clone, + &mostro_key, + message_json, + None, + false, + ) + .await; }); - if let Some(o) = order { - match Order::new(&pool, o, trade_keys, Some(request_id as i64)).await { - Ok(order) => { - println!("Order {} created", order.id.unwrap()); - // Update last trade index to be used in next trade - match User::get(&pool).await { - Ok(mut user) => { - user.set_last_trade_index(trade_index); - if let Err(e) = user.save(&pool).await { - println!("Failed to update user: {}", e); - } - } - Err(e) => println!("Failed to get user: {}", e), - } - } - Err(e) => println!("{}", e), - } - } + + // Wait for the DM to be sent from mostro + wait_for_dm(client, trade_keys, request_id, trade_index, None).await?; Ok(()) } diff --git a/src/cli/take_sell.rs b/src/cli/take_sell.rs index b1a50b7..52b9bc4 100644 --- a/src/cli/take_sell.rs +++ b/src/cli/take_sell.rs @@ -6,9 +6,8 @@ use nostr_sdk::prelude::*; use std::str::FromStr; use uuid::Uuid; -use crate::db::{connect, Order, User}; use crate::lightning::is_valid_invoice; -use crate::util::send_message_sync; +use crate::util::{send_dm, wait_for_dm}; #[allow(clippy::too_many_arguments)] pub async fn execute_take_sell( @@ -65,73 +64,55 @@ pub async fn execute_take_sell( Some(payload), ); - let dm = send_message_sync( - client, - Some(identity_keys), - trade_keys, - mostro_key, - take_sell_message, - true, - false, - ) - .await?; - let pool = connect().await?; + // Send dm to receiver pubkey + println!( + "SENDING DM with trade keys: {:?}", + trade_keys.public_key().to_hex() + ); + let message_json = take_sell_message + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - let order = dm.iter().find_map(|el| { - let message = el.0.get_inner_message_kind(); - if message.request_id == Some(request_id) { - match message.action { - Action::AddInvoice => { - if let Some(Payload::Order(order)) = message.payload.as_ref() { - println!( - "Please add a lightning invoice with amount of {}", - order.amount - ); - return Some(order.clone()); - } - } - Action::CantDo => { - if let Some(Payload::CantDo(Some(cant_do_reason))) = &message.payload { - match cant_do_reason { - CantDoReason::OutOfRangeFiatAmount | CantDoReason::OutOfRangeSatsAmount => { - println!("Error: Amount is outside the allowed range. Please check the order's min/max limits."); - } - _ => { - println!("Unknown reason: {:?}", message.payload); - } - } - } else { - println!("Unknown reason: {:?}", message.payload); - return None; - } - } - _ => { - println!("Unknown action: {:?}", message.action); - return None; - } - } - } - None + // Clone the keys and client for the async call + let identity_keys = identity_keys.clone(); + let trade_keys_clone = trade_keys.clone(); + let client_clone = client.clone(); + // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 + let subscription = Filter::new() + .pubkey(trade_keys.public_key()) + .kind(nostr_sdk::Kind::GiftWrap) + .limit(0); + + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); + + client.subscribe(subscription, Some(opts)).await?; + + // Spawn a new task to send the DM + // This is so we can wait for the gift wrap event in the main thread + tokio::spawn(async move { + let _ = send_dm( + &client_clone, + Some(&identity_keys.clone()), + &trade_keys_clone, + &mostro_key, + message_json, + None, + false, + ) + .await; }); - if let Some(o) = order { - if let Ok(order) = Order::new(&pool, o, trade_keys, Some(request_id as i64)).await { - if let Some(order_id) = order.id { - println!("Order {} created", order_id); - } else { - println!("Warning: The newly created order has no ID."); - } - // Update last trade index to be used in next trade - match User::get(&pool).await { - Ok(mut user) => { - user.set_last_trade_index(trade_index); - if let Err(e) = user.save(&pool).await { - println!("Failed to update user: {}", e); - } - } - Err(e) => println!("Failed to get user: {}", e), - } - } - } + + // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 + let subscription = Filter::new() + .pubkey(trade_keys.public_key()) + .kind(nostr_sdk::Kind::GiftWrap) + .since(Timestamp::from(chrono::Utc::now().timestamp() as u64)) + .limit(2); + + client.subscribe(subscription, None).await?; + + // Wait for the DM to be sent from mostro + wait_for_dm(client, trade_keys, request_id, trade_index, None).await?; Ok(()) } diff --git a/src/pretty_table.rs b/src/pretty_table.rs index 83b88a5..ae68ecf 100644 --- a/src/pretty_table.rs +++ b/src/pretty_table.rs @@ -4,6 +4,8 @@ use comfy_table::presets::UTF8_FULL; use comfy_table::*; use mostro_core::prelude::*; +use crate::util::Event; + pub fn print_order_preview(ord: Payload) -> Result { let single_order = match ord { Payload::Order(o) => o, @@ -77,8 +79,19 @@ pub fn print_order_preview(ord: Payload) -> Result { Ok(table.to_string()) } -pub fn print_orders_table(orders_table: Vec) -> Result { +pub fn print_orders_table(orders_table: Vec) -> Result { let mut table = Table::new(); + // Convert Event to SmallOrder + let orders_table: Vec = orders_table + .into_iter() + .filter_map(|event| { + if let Event::SmallOrder(order) = event { + Some(order) + } else { + None + } + }) + .collect(); //Table rows let mut rows: Vec = Vec::new(); @@ -186,9 +199,21 @@ pub fn print_orders_table(orders_table: Vec) -> Result { Ok(table.to_string()) } -pub fn print_disputes_table(disputes_table: Vec) -> Result { - let mut table = Table::new(); +pub fn print_disputes_table(disputes_table: Vec) -> Result { + // Convert Event to Dispute + let disputes_table: Vec = disputes_table + .into_iter() + .filter_map(|event| { + if let Event::Dispute(dispute) = event { + Some(dispute) + } else { + None + } + }) + .collect(); + // Create table + let mut table = Table::new(); //Table rows let mut rows: Vec = Vec::new(); diff --git a/src/util.rs b/src/util.rs index 4b388d3..d2d4f6b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,5 @@ +use crate::db::{connect, Order, User}; use crate::nip33::{dispute_from_tags, order_from_tags}; - use anyhow::{Error, Result}; use base64::engine::general_purpose; use base64::Engine; @@ -8,9 +8,24 @@ use log::{error, info}; use mostro_core::prelude::*; use nip44::v2::{decrypt_to_bytes, encrypt_to_bytes, ConversationKey}; use nostr_sdk::prelude::*; -use std::thread::sleep; use std::time::Duration; use std::{fs, path::Path}; +use crate::cli::{MOSTRO_KEYS, POOL, TRADE_KEY}; + +#[derive(Clone, Debug)] +pub enum Event { + SmallOrder(SmallOrder), + Dispute(Dispute), // Assuming you have a Dispute struct + MessageTuple((Message, u64)), +} + +#[derive(Clone, Debug)] +pub enum ListKind { + Orders, + Disputes, + DirectMessagesUser, + DirectMessagesAdmin, +} async fn send_gift_wrap_dm_internal( client: &Client, @@ -68,6 +83,160 @@ pub async fn send_gift_wrap_dm( send_gift_wrap_dm_internal(client, trade_keys, receiver_pubkey, message, false).await } +pub async fn save_order( + order: SmallOrder, + trade_keys: &Keys, + request_id: u64, + trade_index: i64, +) -> Result<()> { + let pool = connect().await?; + if let Ok(order) = Order::new(&pool, order, trade_keys, Some(request_id as i64)).await { + if let Some(order_id) = order.id { + println!("Order {} created", order_id); + } else { + println!("Warning: The newly created order has no ID."); + } + // Update last trade index to be used in next trade + match User::get(&pool).await { + Ok(mut user) => { + user.set_last_trade_index(trade_index); + if let Err(e) = user.save(&pool).await { + println!("Failed to update user: {}", e); + } + } + Err(e) => println!("Failed to get user: {}", e), + } + } + Ok(()) +} + +/// Wait for incoming gift wraps or events coming in +pub async fn wait_for_dm( + client: &Client, + trade_keys: &Keys, + request_id: u64, + trade_index: i64, + mut order: Option, +) -> anyhow::Result<()> { + let mut notifications = client.notifications(); + + match tokio::time::timeout(Duration::from_secs(10), async move { + while let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Event { event, .. } = notification { + if event.kind == nostr_sdk::Kind::GiftWrap { + let gift = nip59::extract_rumor(trade_keys, &event).await.unwrap(); + let (message, _): (Message, Option) = serde_json::from_str(&gift.rumor.content).unwrap(); + let message = message.get_inner_message_kind(); + if message.request_id == Some(request_id) { + match message.action { + Action::NewOrder => { + if let Some(Payload::Order(order)) = message.payload.as_ref() { + save_order(order.clone(), trade_keys, request_id, trade_index).await.map_err(|_| ())?; + return Ok(()); + } + } + // this is the case where the buyer adds an invoice to a takesell order + Action::WaitingSellerToPay => { + println!("Now we should wait for the seller to pay the invoice"); + if let Some(mut order) = order.take() { + let pool = connect().await.map_err(|_| ())?; + match order + .set_status(Status::WaitingPayment.to_string()) + .save(&pool) + .await + { + Ok(_) => println!("Order status updated"), + Err(e) => println!("Failed to update order status: {}", e), + } + } + } + // this is the case where the buyer adds an invoice to a takesell order + Action::AddInvoice => { + if let Some(Payload::Order(order)) = &message.payload { + println!( + "Please add a lightning invoice with amount of {}", + order.amount + ); + return Ok(()); + } + } + // this is the case where the buyer pays the invoice coming from a takebuy + Action::PayInvoice => { + if let Some(Payload::PaymentRequest(order, invoice, _)) = &message.payload { + println!( + "Mostro sent you this hold invoice for order id: {}", + order + .as_ref() + .and_then(|o| o.id) + .map_or("unknown".to_string(), |id| id.to_string()) + ); + println!(); + println!("Pay this invoice to continue --> {}", invoice); + println!(); + if let Some(order) = order { + let store_order = order.clone(); + save_order(store_order, trade_keys, request_id, trade_index).await.map_err(|_| ())?; + } + return Ok(()); + } + } + Action::CantDo => { + match message.payload { + Some(Payload::CantDo(Some(CantDoReason::OutOfRangeFiatAmount | CantDoReason::OutOfRangeSatsAmount))) => { + println!("Error: Amount is outside the allowed range. Please check the order's min/max limits."); + return Err(()); + } + Some(Payload::CantDo(Some(CantDoReason::PendingOrderExists))) => { + println!("Error: A pending order already exists. Please wait for it to be filled or canceled."); + return Err(()); + } + Some(Payload::CantDo(Some(CantDoReason::InvalidTradeIndex))) => { + println!("Error: Invalid trade index. Please synchronize the trade index with mostro"); + return Err(()); + } + _ => { + println!("Unknown reason: {:?}", message.payload); + return Err(()); + } + } + } + // this is the case where the user cancels the order + Action::Canceled => { + if let Some(order_id) = &message.id { + // Acquire database connection + let pool = connect().await.map_err(|_| ())?; + // Verify order exists before deletion + if Order::get_by_id(&pool, &order_id.to_string()).await.is_ok() { + Order::delete_by_id(&pool, &order_id.to_string()) + .await + .map_err(|_| ())?; + // Release database connection + drop(pool); + println!("Order {} canceled!", order_id); + return Ok(()); + } else { + println!("Order not found: {}", order_id); + return Err(()); + } + } + } + _ => { + println!("Unknown action: {:?}", message.action); + return Err(()); + } + } + } + } + } + } + Ok(()) + }) + .await { + Ok(_) => Ok(()), + Err(_) => Err(anyhow::anyhow!("Timeout waiting for DM or gift wrap event")) + } +} + pub async fn send_dm( client: &Client, identity_keys: Option<&Keys>, @@ -133,7 +302,6 @@ pub async fn send_dm( EventBuilder::gift_wrap(identity_keys, receiver_pubkey, rumor, tags).await? }; - info!("Sending event: {event:#?}"); client.send_event(&event).await?; Ok(()) @@ -150,6 +318,7 @@ pub async fn connect_nostr() -> Result { for r in relays.into_iter() { client.add_relay(r).await?; } + // Connect to relays and keep connection alive client.connect().await; @@ -162,9 +331,9 @@ pub async fn send_message_sync( trade_keys: &Keys, receiver_pubkey: PublicKey, message: Message, - wait_for_dm: bool, + _wait_for_dm: bool, to_user: bool, -) -> Result> { +) -> Result<()> { let message_json = message .as_json() .map_err(|_| Error::msg("Failed to serialize message"))?; @@ -183,16 +352,8 @@ pub async fn send_message_sync( to_user, ) .await?; - // FIXME: This is a hack to wait for the DM to be sent - sleep(Duration::from_secs(2)); - - let dm: Vec<(Message, u64)> = if wait_for_dm { - get_direct_messages(client, trade_keys, 15, to_user, None).await - } else { - Vec::new() - }; - Ok(dm) + Ok(()) } pub async fn get_direct_messages_from_trade_keys( @@ -217,65 +378,13 @@ pub async fn get_direct_messages_from_trade_keys( .unwrap() .timestamp() as u64; - let mut all_direct_messages: Vec<(Message, u64, PublicKey)> = Vec::new(); - let mut id_set = std::collections::HashSet::::new(); - - for trade_key_hex in trade_keys_hex { - if let Ok(trade_keys) = Keys::parse(&trade_key_hex) { - let filters = Filter::new() - .kind(nostr_sdk::Kind::GiftWrap) - .pubkey(trade_keys.public_key()) - .since(fake_timestamp); - - info!("Request events with event kind : {:?} for trade key: {}", - filters.kinds, trade_keys.public_key()); - - if let Ok(events) = client.fetch_events(filters, Duration::from_secs(15)).await { - for dm in events.iter() { - if !id_set.insert(dm.id) { - continue; // Already processed - } - - let unwrapped_gift = match nip59::extract_rumor(&trade_keys, dm).await { - Ok(u) => u, - Err(_) => { - error!("Error unwrapping gift for trade key: {}", trade_keys.public_key()); - continue; - } - }; - - // Filter: only process messages NOT from Mostro (user-to-user messages) - if unwrapped_gift.rumor.pubkey == *mostro_pubkey { - continue; // Skip Mostro messages - } - - if unwrapped_gift.rumor.created_at.as_u64() < since_time { - continue; - } - - // Parse JSON content (all messages should be JSON now) - let (message, _): (Message, Option) = match serde_json::from_str(&unwrapped_gift.rumor.content) { - Ok(parsed) => parsed, - Err(_) => { - error!("Error parsing JSON content from: {}", unwrapped_gift.rumor.pubkey); - continue; - } - }; - - all_direct_messages.push(( - message, - unwrapped_gift.rumor.created_at.as_u64(), - unwrapped_gift.rumor.pubkey - )); - } - } - } else { - error!("Failed to parse trade key: {}", trade_key_hex); - } - } + let dm: Vec<(Message, u64)> = if wait_for_dm { + get_direct_messages(client, trade_keys, 15, to_user).await + } else { + Vec::new() + }; - all_direct_messages.sort_by(|a, b| a.1.cmp(&b.1)); - all_direct_messages + Ok(()) } pub async fn get_direct_messages( @@ -385,47 +494,100 @@ pub async fn get_direct_messages( direct_messages } -pub async fn get_orders_list( - pubkey: PublicKey, - status: Status, - currency: Option, - kind: Option, - client: &Client, -) -> Result> { - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::days(7)) - .unwrap() - .timestamp() as u64; +fn parse_dispute_events(events: Events) -> Vec { + // Extracted Disputes List + let mut disputes_list = Vec::::new(); - let timestamp = Timestamp::from(since_time); + // Scan events to extract all disputes + for event in events.into_iter() { + if let Ok(mut dispute) = dispute_from_tags(event.tags) { + info!("Found Dispute id : {:?}", dispute.id); + // Get created at field from Nostr event + dispute.created_at = event.created_at.as_u64() as i64; + disputes_list.push(dispute.clone()); + } + } - let filters = Filter::new() - .author(pubkey) - .limit(50) - .since(timestamp) - .custom_tag(SingleLetterTag::lowercase(Alphabet::Z), "order".to_string()) - .kind(nostr_sdk::Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND)); + let buffer_dispute_list = disputes_list.clone(); + // Order all element ( orders ) received to filter - discard disaligned messages + // if an order has an older message with the state we received is discarded for the latest one + disputes_list.retain(|keep| { + !buffer_dispute_list + .iter() + .any(|x| x.id == keep.id && x.created_at > keep.created_at) + }); - info!( - "Request to mostro id : {:?} with event kind : {:?} ", - filters.authors, filters.kinds - ); + // Sort by id to remove duplicates + disputes_list.sort_by(|a, b| b.id.cmp(&a.id)); + disputes_list.dedup_by(|a, b| a.id == b.id); + + // Finally sort list by creation time + disputes_list.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + disputes_list +} + + +async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message,u64)>{ + // Buffer vector for direct messages + // Vector for single order id check - maybe multiple relay could send the same order id? Check unique one... + let mut id_list = Vec::::new(); + // Vector for direct messages + let mut direct_messages: Vec<(Message, u64)> = Vec::new(); + + for dm in events.iter() { + if !id_list.contains(&dm.id) { + id_list.push(dm.id); + + let unwrapped_gift = match nip59::extract_rumor(pubkey, dm).await { + Ok(u) => u, + Err(_) => { + println!("Error unwrapping gift"); + continue; + } + }; + let (message, _): (Message, Option) = + serde_json::from_str(&unwrapped_gift.rumor.content).unwrap(); + + // Create a tuple with the created_at and the message + let (created_at, message) = (unwrapped_gift.rumor.created_at, message); + + + // Here we discard messages older than the real since parameter + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(30)) + .unwrap() + .timestamp() as u64; + if created_at.as_u64() < since_time { + continue; + } + direct_messages.push((message, created_at.as_u64())); + } + } + // Return element sorted by second tuple element ( Timestamp ) + direct_messages.sort_by(|a, b| a.1.cmp(&b.1)); + + direct_messages +} +fn parse_orders_events( + events: Events, + currency: Option, + status: Option, + kind: Option, +) -> Vec { // Extracted Orders List let mut complete_events_list = Vec::::new(); let mut requested_orders_list = Vec::::new(); - // Send all requests to relays - if let Ok(mostro_req) = client.fetch_events(filters, Duration::from_secs(15)).await { - // Scan events to extract all orders - for el in mostro_req.iter() { - let order = order_from_tags(el.tags.clone()); + // Scan events to extract all orders + for event in events.iter() { + let order = order_from_tags(event.tags.clone()); - if order.is_err() { - error!("{order:?}"); - continue; - } - let mut order = order?; + if order.is_err() { + error!("{order:?}"); + continue; + } + if let Ok(mut order) = order { info!("Found Order id : {:?}", order.id.unwrap()); @@ -445,26 +607,22 @@ pub async fn get_orders_list( } // Get created at field from Nostr event - order.created_at = Some(el.created_at.as_u64() as i64); - + order.created_at = Some(event.created_at.as_u64() as i64); complete_events_list.push(order.clone()); - - if order.status.ne(&Some(status)) { + if order.status.ne(&status) { continue; - } + } - if currency.is_some() && order.fiat_code.ne(¤cy.clone().unwrap()) { - continue; - } + if currency.is_some() && order.fiat_code.ne(¤cy.clone().unwrap()) { + continue; + } - if kind.is_some() && order.kind.ne(&kind) { - continue; - } - // Add just requested orders requested by filtering - requested_orders_list.push(order); + if kind.is_some() && order.kind.ne(&kind) { + continue; } + // Add just requested orders requested by filtering + requested_orders_list.push(order); } - // Order all element ( orders ) received to filter - discard disaligned messages // if an order has an older message with the state we received is discarded for the latest one requested_orders_list.retain(|keep| { @@ -475,11 +633,118 @@ pub async fn get_orders_list( // Sort by id to remove duplicates requested_orders_list.sort_by(|a, b| b.id.cmp(&a.id)); requested_orders_list.dedup_by(|a, b| a.id == b.id); - + } // Finally sort list by creation time requested_orders_list.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + requested_orders_list +} + +fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { + match list_kind { + ListKind::Orders => { + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::days(7)) + .unwrap() + .timestamp() as u64; + + let timestamp = Timestamp::from(since_time); + + // Create filter for fetching orders + Filter::new() + .author(pubkey) + .limit(50) + .since(timestamp) + .custom_tag(SingleLetterTag::lowercase(Alphabet::Z), "order".to_string()) + .kind(nostr_sdk::Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND)) + } + ListKind::Disputes => { + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::days(7)) + .unwrap() + .timestamp() as u64; + + let timestamp = Timestamp::from(since_time); + + // Create filter for fetching orders + Filter::new() + .author(pubkey) + .limit(50) + .since(timestamp) + .custom_tag( + SingleLetterTag::lowercase(Alphabet::Z), + "dispute".to_string(), + ) + .kind(nostr_sdk::Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND)) + } + ListKind::DirectMessagesAdmin => { + // We use a fake timestamp to thwart time-analysis attacks + let fake_since = 2880; + let fake_since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(fake_since)) + .unwrap() + .timestamp() as u64; + + let fake_timestamp = Timestamp::from(fake_since_time); - Ok(requested_orders_list) + Filter::new() + .kind(nostr_sdk::Kind::GiftWrap) + .pubkey(pubkey) + .since(fake_timestamp) + } + ListKind::DirectMessagesUser => { + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(30)) + .unwrap() + .timestamp() as u64; + let timestamp = Timestamp::from(since_time); + Filter::new() + .kind(nostr_sdk::Kind::PrivateDirectMessage) + .pubkey(pubkey) + .since(timestamp) + } + } +} + + +pub async fn fetch_events_list( + list_kind: ListKind, + status: Option, + currency: Option, + kind: Option, + client: &Client, +) -> Result> { + match list_kind { + ListKind::Orders => { + let filters = create_filter(list_kind, MOSTRO_KEYS.get().unwrap().public_key()); + let fetched_events = client.fetch_events(filters, Duration::from_secs(15)).await?; + let orders = parse_orders_events(fetched_events, currency, status, kind); + Ok(orders.into_iter().map(Event::SmallOrder).collect()) + } + ListKind::DirectMessagesAdmin => { + let filters = create_filter(list_kind, MOSTRO_KEYS.get().unwrap().public_key()); + let fetched_events = client.fetch_events(filters, Duration::from_secs(15)).await?; + let direct_messages_mostro = parse_dm_events(fetched_events, MOSTRO_KEYS.get().unwrap()).await; + Ok(direct_messages_mostro.into_iter().map(Event::MessageTuple).collect()) + } + ListKind::DirectMessagesUser => { + let trade_index = TRADE_KEY.get().unwrap().1; + let mut direct_messages: Vec<(Message, u64)> = Vec::new(); + for index in 1..=trade_index { + let trade_key = User::get_trade_keys(&POOL.get().unwrap(), index).await?; + let filter = create_filter(ListKind::DirectMessagesUser, trade_key.public_key()); + let fetched_user_messages = client.fetch_events(filter, Duration::from_secs(15)).await?; + let direct_messages_for_trade_key = parse_dm_events(fetched_user_messages, &trade_key).await; + direct_messages.extend(direct_messages_for_trade_key); + } + Ok(direct_messages.into_iter().map(Event::MessageTuple).collect()) + }, + ListKind::Disputes => { + let filters = create_filter(list_kind, MOSTRO_KEYS.get().unwrap().public_key()); + let fetched_events = client.fetch_events(filters, Duration::from_secs(15)).await?; + let disputes = parse_dispute_events(fetched_events); + Ok(disputes.into_iter().map(Event::Dispute).collect()) + } + } } pub async fn get_disputes_list(pubkey: PublicKey, client: &Client) -> Result> { From 9e6f6ede4f90beadef73a23a75e55f2cdde97330 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Sun, 7 Sep 2025 21:27:37 +0200 Subject: [PATCH 02/19] refactor: big restyle all around the codebase - to be fully tested --- src/cli.rs | 263 ++++++---- src/cli/get_dm.rs | 22 +- src/cli/list_disputes.rs | 25 +- src/cli/list_orders.rs | 20 +- src/cli/new_order.rs | 2 +- src/lib.rs | 2 +- src/parser/disputes.rs | 116 +++++ src/parser/dms.rs | 77 +++ src/parser/mod.rs | 7 + src/{pretty_table.rs => parser/orders.rs} | 155 +++--- src/util.rs | 567 +++++++--------------- 11 files changed, 678 insertions(+), 578 deletions(-) create mode 100644 src/parser/disputes.rs create mode 100644 src/parser/dms.rs create mode 100644 src/parser/mod.rs rename src/{pretty_table.rs => parser/orders.rs} (73%) diff --git a/src/cli.rs b/src/cli.rs index 458f665..4427a38 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -27,7 +27,6 @@ use crate::cli::new_order::execute_new_order; use crate::cli::rate_user::execute_rate_user; use crate::cli::restore::execute_restore; use crate::cli::send_dm::execute_send_dm; -use crate::cli::send_msg::execute_send_msg; use crate::cli::take_buy::execute_take_buy; use crate::cli::take_dispute::execute_take_dispute; use crate::cli::take_sell::execute_take_sell; @@ -37,6 +36,7 @@ use crate::util; use anyhow::{Error, Result}; use clap::{Parser, Subcommand}; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; use std::sync::OnceLock; use std::{ env::{set_var, var}, @@ -44,7 +44,6 @@ use std::{ }; use take_dispute::*; use uuid::Uuid; -use sqlx::SqlitePool; pub static IDENTITY_KEYS: OnceLock = OnceLock::new(); pub static MOSTRO_KEYS: OnceLock = OnceLock::new(); @@ -52,6 +51,16 @@ pub static MOSTRO_PUBKEY: OnceLock = OnceLock::new(); pub static POOL: OnceLock = OnceLock::new(); pub static TRADE_KEY: OnceLock<(Keys, i64)> = OnceLock::new(); +pub struct Context { + pub client: Client, + pub identity_keys: Keys, + pub trade_keys: Keys, + pub trade_index: i64, + pub pool: SqlitePool, + pub mostro_keys: Keys, + pub mostro_pubkey: PublicKey, +} + #[derive(Parser)] #[command( name = "mostro-cli", @@ -299,16 +308,16 @@ fn get_env_var(cli: &Cli) { } if cli.mostropubkey.is_some() { - set_var("MOSTRO_PUBKEY", cli.mostropubkey.unwrap()); + set_var("MOSTRO_PUBKEY", cli.mostropubkey.clone().unwrap()); } - let pubkey = var("MOSTRO_PUBKEY").expect("$MOSTRO_PUBKEY env var needs to be set"); + let _pubkey = var("MOSTRO_PUBKEY").expect("$MOSTRO_PUBKEY env var needs to be set"); if cli.relays.is_some() { - set_var("RELAYS", cli.relays.unwrap()); + set_var("RELAYS", cli.relays.clone().unwrap()); } if cli.pow.is_some() { - set_var("POW", cli.pow.unwrap()); + set_var("POW", cli.pow.clone().unwrap()); } if cli.secret { @@ -359,47 +368,82 @@ fn check_fiat_range(s: &str) -> Result<(i64, Option)> { pub async fn run() -> Result<()> { let cli = Cli::parse(); + let ctx = init_context(&cli).await?; + + if let Some(cmd) = &cli.command { + cmd.run(&ctx).await?; + } + + println!("Bye Bye!"); + + Ok(()) +} + +async fn init_context(cli: &Cli) -> Result { // Get environment variables - get_env_var(&cli); + get_env_var(cli); - // Set pool as global variable to reuse in next calls + // Initialize database pool let pool = connect().await?; POOL.get_or_init(|| pool.clone()); + // Get identity keys let identity_keys = User::get_identity_keys(&pool) .await .map_err(|e| anyhow::anyhow!("Failed to get identity keys: {}", e))?; - - // Set identity keys as global variable to reuse in next calls IDENTITY_KEYS.get_or_init(|| identity_keys.clone()); - // Get next trade keys + // Get trade keys let (trade_keys, trade_index) = User::get_next_trade_keys(&pool) .await .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?; - - // Set trade keys as global variable to reuse in next calls TRADE_KEY.get_or_init(|| (trade_keys.clone(), trade_index)); - // Mostro pubkey - let mostro_key = Keys::from_str(&std::env::var("NSEC_PRIVKEY").map_err(|e| anyhow::anyhow!("Failed to get mostro keys: {}", e))?)?; - // Set mostro keys as global variable to reuse in next calls - MOSTRO_KEYS.get_or_init(|| mostro_key.clone()); - MOSTRO_PUBKEY.get_or_init(|| mostro_key.public_key()); + // Get Mostro admin keys + let mostro_keys = Keys::from_str( + &std::env::var("NSEC_PRIVKEY") + .map_err(|e| anyhow::anyhow!("Failed to get mostro keys: {}", e))?, + )?; + MOSTRO_KEYS.get_or_init(|| mostro_keys.clone()); + MOSTRO_PUBKEY.get_or_init(|| mostro_keys.public_key()); - // Call function to connect to relays + // Connect to Nostr relays let client = util::connect_nostr().await?; - if let Some(cmd) = cli.command { - match &cmd { + Ok(Context { + client, + identity_keys, + trade_keys, + trade_index, + pool, + mostro_keys: mostro_keys.clone(), + mostro_pubkey: mostro_keys.public_key(), + }) +} + +impl Commands { + pub async fn run(&self, ctx: &Context) -> Result<()> { + match self { Commands::ConversationKey { pubkey } => { - execute_conversation_key(&trade_keys, PublicKey::from_str(pubkey)?).await? + execute_conversation_key(&ctx.trade_keys, PublicKey::from_str(pubkey)?).await } Commands::ListOrders { status, currency, kind, - } => execute_list_orders(kind, currency, status, &client).await?, + } => { + execute_list_orders( + kind, + currency, + status, + ctx.mostro_pubkey, + &ctx.mostro_keys, + ctx.trade_index, + &ctx.pool, + &ctx.client, + ) + .await + } Commands::TakeSell { order_id, invoice, @@ -409,60 +453,83 @@ pub async fn run() -> Result<()> { order_id, invoice, *amount, - &identity_keys, - &trade_keys, - trade_index, - mostro_key, - &client, + &ctx.identity_keys, + &ctx.trade_keys, + ctx.trade_index, + ctx.mostro_pubkey, + &ctx.client, ) - .await? + .await } Commands::TakeBuy { order_id, amount } => { execute_take_buy( order_id, *amount, - &identity_keys, - &trade_keys, - trade_index, - mostro_key, - &client, + &ctx.identity_keys, + &ctx.trade_keys, + ctx.trade_index, + ctx.mostro_pubkey, + &ctx.client, ) - .await? + .await } Commands::AddInvoice { order_id, invoice } => { - execute_add_invoice(order_id, invoice, &identity_keys, mostro_key, &client).await? + execute_add_invoice( + order_id, + invoice, + &ctx.identity_keys, + ctx.mostro_pubkey, + &ctx.client, + ) + .await } Commands::GetDm { since, from_user } => { - execute_get_dm(since, trade_index, &client, *from_user, false, &mostro_key).await? + execute_get_dm( + since, + ctx.trade_index, + &ctx.mostro_keys, + &ctx.client, + *from_user, + false, &mostro_key).await? } Commands::GetDmUser { since } => { - execute_get_dm_user(since, &client, &mostro_key).await? + execute_get_dm_user(since, &client, &mostro_key, + ) + .await } Commands::GetAdminDm { since, from_user } => { - execute_get_dm(since, trade_index, &client, *from_user, true, &mostro_key).await? + execute_get_dm( + since, + ctx.trade_index, + &ctx.mostro_keys, + &ctx.client, + *from_user, + true, + ) + .await } Commands::FiatSent { order_id } | Commands::Release { order_id } | Commands::Dispute { order_id } | Commands::Cancel { order_id } => { - execute_send_msg( - cmd.clone(), - Some(*order_id), - Some(&identity_keys), - mostro_key, - &client, - None, + crate::util::run_simple_order_msg( + self.clone(), + order_id, + &ctx.identity_keys, + ctx.mostro_pubkey, + &ctx.client, ) - .await? + .await } Commands::AdmAddSolver { npubkey } => { - let id_key = match std::env::var("NSEC_PRIVKEY") { - Ok(id_key) => Keys::parse(&id_key)?, - Err(e) => { - anyhow::bail!("NSEC_PRIVKEY not set: {e}"); - } - }; - execute_admin_add_solver(npubkey, &id_key, &trade_keys, mostro_key, &client).await? + execute_admin_add_solver( + npubkey, + &ctx.mostro_keys, + &ctx.trade_keys, + ctx.mostro_pubkey, + &ctx.client, + ) + .await } Commands::NewOrder { kind, @@ -482,59 +549,75 @@ pub async fn run() -> Result<()> { payment_method, premium, invoice, - &identity_keys, - &trade_keys, - trade_index, - mostro_key, - &client, + &ctx.identity_keys, + &ctx.trade_keys, + ctx.trade_index, + ctx.mostro_pubkey, + &ctx.client, expiration_days, ) - .await? + .await } Commands::Rate { order_id, rating } => { - execute_rate_user(order_id, rating, &identity_keys, mostro_key, &client).await?; + execute_rate_user( + order_id, + rating, + &ctx.identity_keys, + ctx.mostro_pubkey, + &ctx.client, + ) + .await } Commands::Restore {} => { execute_restore(&identity_keys, mostro_key, &client).await?; } Commands::AdmSettle { order_id } => { - let id_key = match std::env::var("NSEC_PRIVKEY") { - Ok(id_key) => Keys::parse(&id_key)?, - Err(e) => { - anyhow::bail!("NSEC_PRIVKEY not set: {e}"); - } - }; - execute_admin_settle_dispute(order_id, &id_key, &trade_keys, mostro_key, &client) - .await?; + execute_admin_settle_dispute( + order_id, + &ctx.mostro_keys, + &ctx.trade_keys, + ctx.mostro_pubkey, + &ctx.client, + ) + .await } Commands::AdmCancel { order_id } => { - let id_key = match std::env::var("NSEC_PRIVKEY") { - Ok(id_key) => Keys::parse(&id_key)?, - Err(e) => { - anyhow::bail!("NSEC_PRIVKEY not set: {e}"); - } - }; - execute_admin_cancel_dispute(order_id, &id_key, &trade_keys, mostro_key, &client) - .await?; + execute_admin_cancel_dispute( + order_id, + &ctx.mostro_keys, + &ctx.trade_keys, + ctx.mostro_pubkey, + &ctx.client, + ) + .await } Commands::AdmTakeDispute { dispute_id } => { - let id_key = match std::env::var("NSEC_PRIVKEY") { - Ok(id_key) => Keys::parse(&id_key)?, - Err(e) => { - anyhow::bail!("NSEC_PRIVKEY not set: {e}"); - } - }; - - execute_take_dispute(dispute_id, &id_key, &trade_keys, mostro_key, &client).await? + execute_take_dispute( + dispute_id, + &ctx.mostro_keys, + &ctx.trade_keys, + ctx.mostro_pubkey, + &ctx.client, + ) + .await + } + Commands::AdmListDisputes {} => { + execute_list_disputes( + ctx.mostro_pubkey, + &ctx.mostro_keys, + ctx.trade_index, + &ctx.pool, + &ctx.client, + ) + .await } - Commands::AdmListDisputes {} => execute_list_disputes(mostro_key, &client).await?, Commands::SendDm { pubkey, order_id, message, } => { let pubkey = PublicKey::from_str(pubkey)?; - execute_send_dm(pubkey, &client, order_id, message).await? + execute_send_dm(pubkey, &ctx.client, order_id, message).await } Commands::DmToUser { pubkey, @@ -548,10 +631,6 @@ pub async fn run() -> Result<()> { let pubkey = PublicKey::from_str(pubkey)?; execute_adm_send_dm(pubkey, &client, message).await? } - }; + } } - - println!("Bye Bye!"); - - Ok(()) } diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index db7b631..031a3cf 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -2,18 +2,19 @@ use anyhow::Result; use chrono::DateTime; use mostro_core::prelude::*; use nostr_sdk::prelude::*; -use crate::cli::MOSTRO_KEYS; use crate::{ db::{connect, Order, User}, - util::get_direct_messages, + parser::dms::parse_dm_events, + util::{create_filter, ListKind}, }; pub async fn execute_get_dm( - since: &i64, + _since: &i64, trade_index: i64, + mostro_keys: &Keys, client: &Client, - from_user: bool, + _from_user: bool, admin: bool, mostro_pubkey: &PublicKey, ) -> Result<()> { @@ -22,12 +23,19 @@ pub async fn execute_get_dm( if !admin { for index in 1..=trade_index { let keys = User::get_trade_keys(&pool, index).await?; - let dm_temp = - get_direct_messages(client, &keys, *since, from_user, Some(mostro_pubkey)).await; + let filter = create_filter(ListKind::DirectMessagesUser, keys.public_key()); + let fetched_events = client + .fetch_events(filter, std::time::Duration::from_secs(15)) + .await?; + let dm_temp = parse_dm_events(fetched_events, &keys).await; dm.extend(dm_temp); } } else { - let dm_temp = get_direct_messages(client, MOSTRO_KEYS.get().unwrap(), *since, from_user).await; + let filter = create_filter(ListKind::DirectMessagesAdmin, mostro_keys.public_key()); + let fetched_events = client + .fetch_events(filter, std::time::Duration::from_secs(15)) + .await?; + let dm_temp = parse_dm_events(fetched_events, mostro_keys).await; dm.extend(dm_temp); } diff --git a/src/cli/list_disputes.rs b/src/cli/list_disputes.rs index 0cc4cc5..47c7056 100644 --- a/src/cli/list_disputes.rs +++ b/src/cli/list_disputes.rs @@ -1,18 +1,35 @@ use anyhow::Result; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; -use crate::pretty_table::print_disputes_table; +use crate::parser::disputes::print_disputes_table; use crate::util::{fetch_events_list, ListKind}; -pub async fn execute_list_disputes(mostro_key: PublicKey, client: &Client) -> Result<()> { +pub async fn execute_list_disputes( + mostro_key: PublicKey, + mostro_keys: &Keys, + trade_index: i64, + pool: &SqlitePool, + client: &Client, +) -> Result<()> { println!( "Requesting disputes from mostro pubId - {}", mostro_key.clone() ); // Get orders from relays - let table_of_disputes = - fetch_events_list(mostro_key, ListKind::Disputes, None, None, None, client).await?; + let table_of_disputes = fetch_events_list( + ListKind::Disputes, + None, + None, + None, + mostro_key, + mostro_keys, + trade_index, + pool, + client, + ) + .await?; let table = print_disputes_table(table_of_disputes)?; println!("{table}"); diff --git a/src/cli/list_orders.rs b/src/cli/list_orders.rs index f81eeaf..ac2cccd 100644 --- a/src/cli/list_orders.rs +++ b/src/cli/list_orders.rs @@ -1,15 +1,20 @@ +use crate::parser::orders::print_orders_table; +use crate::util::{fetch_events_list, ListKind}; use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; use std::str::FromStr; -use crate::cli::MOSTRO_PUBKEY; -use crate::pretty_table::print_orders_table; -use crate::util::{fetch_events_list, ListKind}; +#[allow(clippy::too_many_arguments)] pub async fn execute_list_orders( kind: &Option, currency: &Option, status: &Option, + mostro_pubkey: PublicKey, + mostro_keys: &Keys, + trade_index: i64, + pool: &SqlitePool, client: &Client, ) -> Result<()> { // Used to get upper currency string to check against a list of tickers @@ -43,10 +48,7 @@ pub async fn execute_list_orders( ); } - println!( - "Requesting orders from mostro pubId - {}", - MOSTRO_PUBKEY.get().unwrap() - ); + println!("Requesting orders from mostro pubId - {}", mostro_pubkey); // Get orders from relays let table_of_orders = fetch_events_list( @@ -54,6 +56,10 @@ pub async fn execute_list_orders( status_checked, upper_currency, kind_checked, + mostro_pubkey, + mostro_keys, + trade_index, + pool, client, ) .await?; diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index 3ad51c2..e708d42 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -7,7 +7,7 @@ use std::process; use std::str::FromStr; use uuid::Uuid; -use crate::pretty_table::print_order_preview; +use crate::parser::orders::print_order_preview; use crate::util::{send_dm, uppercase_first, wait_for_dm}; pub type FiatNames = HashMap; diff --git a/src/lib.rs b/src/lib.rs index ce39579..14986a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,5 +3,5 @@ pub mod db; pub mod error; pub mod lightning; pub mod nip33; -pub mod pretty_table; +pub mod parser; pub mod util; diff --git a/src/parser/disputes.rs b/src/parser/disputes.rs new file mode 100644 index 0000000..db2a3f0 --- /dev/null +++ b/src/parser/disputes.rs @@ -0,0 +1,116 @@ +use anyhow::Result; +use chrono::DateTime; +use comfy_table::presets::UTF8_FULL; +use comfy_table::*; +use log::info; +use mostro_core::prelude::*; +use nostr_sdk::prelude::*; + +use crate::util::Event; + +use crate::nip33::dispute_from_tags; + +pub fn parse_dispute_events(events: Events) -> Vec { + // Extracted Disputes List + let mut disputes_list = Vec::::new(); + + // Scan events to extract all disputes + for event in events.into_iter() { + if let Ok(mut dispute) = dispute_from_tags(event.tags) { + info!("Found Dispute id : {:?}", dispute.id); + // Get created at field from Nostr event + dispute.created_at = event.created_at.as_u64() as i64; + disputes_list.push(dispute.clone()); + } + } + + let buffer_dispute_list = disputes_list.clone(); + // Order all element ( orders ) received to filter - discard disaligned messages + // if an order has an older message with the state we received is discarded for the latest one + disputes_list.retain(|keep| { + !buffer_dispute_list + .iter() + .any(|x| x.id == keep.id && x.created_at > keep.created_at) + }); + + // Sort by id to remove duplicates + disputes_list.sort_by(|a, b| b.id.cmp(&a.id)); + disputes_list.dedup_by(|a, b| a.id == b.id); + + // Finally sort list by creation time + disputes_list.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + disputes_list +} + +pub fn print_disputes_table(disputes_table: Vec) -> Result { + // Convert Event to Dispute + let disputes_table: Vec = disputes_table + .into_iter() + .filter_map(|event| { + if let Event::Dispute(dispute) = event { + Some(dispute) + } else { + None + } + }) + .collect(); + + // Create table + let mut table = Table::new(); + //Table rows + let mut rows: Vec = Vec::new(); + + if disputes_table.is_empty() { + table + .load_preset(UTF8_FULL) + .set_content_arrangement(ContentArrangement::Dynamic) + .set_width(160) + .set_header(vec![Cell::new("Sorry...") + .add_attribute(Attribute::Bold) + .set_alignment(CellAlignment::Center)]); + + // Single row for error + let mut r = Row::new(); + + r.add_cell( + Cell::new("No disputes found with requested parameters...") + .fg(Color::Red) + .set_alignment(CellAlignment::Center), + ); + + //Push single error row + rows.push(r); + } else { + table + .load_preset(UTF8_FULL) + .set_content_arrangement(ContentArrangement::Dynamic) + .set_width(160) + .set_header(vec![ + Cell::new("Dispute Id") + .add_attribute(Attribute::Bold) + .set_alignment(CellAlignment::Center), + Cell::new("Status") + .add_attribute(Attribute::Bold) + .set_alignment(CellAlignment::Center), + Cell::new("Created") + .add_attribute(Attribute::Bold) + .set_alignment(CellAlignment::Center), + ]); + + //Iterate to create table of orders + for single_dispute in disputes_table.into_iter() { + let date = DateTime::from_timestamp(single_dispute.created_at, 0); + + let r = Row::from(vec![ + Cell::new(single_dispute.id).set_alignment(CellAlignment::Center), + Cell::new(single_dispute.status.to_string()).set_alignment(CellAlignment::Center), + Cell::new(date.unwrap()), + ]); + rows.push(r); + } + } + + table.add_rows(rows); + + Ok(table.to_string()) +} diff --git a/src/parser/dms.rs b/src/parser/dms.rs new file mode 100644 index 0000000..a896f32 --- /dev/null +++ b/src/parser/dms.rs @@ -0,0 +1,77 @@ +use base64::engine::general_purpose; +use base64::Engine; +use mostro_core::prelude::*; +use nip44::v2::{decrypt_to_bytes, ConversationKey}; +use nostr_sdk::prelude::*; + +pub async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message, u64)> { + let mut id_list = Vec::::new(); + let mut direct_messages: Vec<(Message, u64)> = Vec::new(); + + for dm in events.iter() { + if !id_list.contains(&dm.id) { + id_list.push(dm.id); + + let (created_at, message) = match dm.kind { + nostr_sdk::Kind::GiftWrap => { + let unwrapped_gift = match nip59::extract_rumor(pubkey, dm).await { + Ok(u) => u, + Err(_) => { + println!("Error unwrapping gift"); + continue; + } + }; + let (message, _): (Message, Option) = + serde_json::from_str(&unwrapped_gift.rumor.content).unwrap(); + (unwrapped_gift.rumor.created_at, message) + } + nostr_sdk::Kind::PrivateDirectMessage => { + let ck = + if let Ok(ck) = ConversationKey::derive(pubkey.secret_key(), &dm.pubkey) { + ck + } else { + continue; + }; + let b64decoded_content = + match general_purpose::STANDARD.decode(dm.content.as_bytes()) { + Ok(b64decoded_content) => b64decoded_content, + Err(_) => { + continue; + } + }; + let unencrypted_content = match decrypt_to_bytes(&ck, &b64decoded_content) { + Ok(bytes) => bytes, + Err(_) => { + continue; + } + }; + let message_str = match String::from_utf8(unencrypted_content) { + Ok(s) => s, + Err(_) => { + continue; + } + }; + let message = match Message::from_json(&message_str) { + Ok(m) => m, + Err(_) => { + continue; + } + }; + (dm.created_at, message) + } + _ => continue, + }; + + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(30)) + .unwrap() + .timestamp() as u64; + if created_at.as_u64() < since_time { + continue; + } + direct_messages.push((message, created_at.as_u64())); + } + } + direct_messages.sort_by(|a, b| a.1.cmp(&b.1)); + direct_messages +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs new file mode 100644 index 0000000..3bc1427 --- /dev/null +++ b/src/parser/mod.rs @@ -0,0 +1,7 @@ +pub mod disputes; +pub mod dms; +pub mod orders; + +pub use disputes::parse_dispute_events; +pub use dms::parse_dm_events; +pub use orders::parse_orders_events; diff --git a/src/pretty_table.rs b/src/parser/orders.rs similarity index 73% rename from src/pretty_table.rs rename to src/parser/orders.rs index ae68ecf..ee4e9d1 100644 --- a/src/pretty_table.rs +++ b/src/parser/orders.rs @@ -1,10 +1,82 @@ +use crate::util::Event; use anyhow::Result; use chrono::DateTime; use comfy_table::presets::UTF8_FULL; use comfy_table::*; +use log::{error, info}; use mostro_core::prelude::*; +use nostr_sdk::prelude::*; -use crate::util::Event; +use crate::nip33::order_from_tags; + +pub fn parse_orders_events( + events: Events, + currency: Option, + status: Option, + kind: Option, +) -> Vec { + // Extracted Orders List + let mut complete_events_list = Vec::::new(); + let mut requested_orders_list = Vec::::new(); + + // Scan events to extract all orders + for event in events.iter() { + let order = order_from_tags(event.tags.clone()); + + if order.is_err() { + error!("{order:?}"); + continue; + } + if let Ok(mut order) = order { + info!("Found Order id : {:?}", order.id.unwrap()); + + if order.id.is_none() { + info!("Order ID is none"); + continue; + } + + if order.kind.is_none() { + info!("Order kind is none"); + continue; + } + + if order.status.is_none() { + info!("Order status is none"); + continue; + } + + // Get created at field from Nostr event + order.created_at = Some(event.created_at.as_u64() as i64); + complete_events_list.push(order.clone()); + if order.status.ne(&status) { + continue; + } + + if currency.is_some() && order.fiat_code.ne(¤cy.clone().unwrap()) { + continue; + } + + if kind.is_some() && order.kind.ne(&kind) { + continue; + } + // Add just requested orders requested by filtering + requested_orders_list.push(order); + } + // Order all element ( orders ) received to filter - discard disaligned messages + // if an order has an older message with the state we received is discarded for the latest one + requested_orders_list.retain(|keep| { + !complete_events_list + .iter() + .any(|x| x.id == keep.id && x.created_at > keep.created_at) + }); + // Sort by id to remove duplicates + requested_orders_list.sort_by(|a, b| b.id.cmp(&a.id)); + requested_orders_list.dedup_by(|a, b| a.id == b.id); + } + // Finally sort list by creation time + requested_orders_list.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + requested_orders_list +} pub fn print_order_preview(ord: Payload) -> Result { let single_order = match ord { @@ -43,10 +115,10 @@ pub fn print_order_preview(ord: Payload) -> Result { let r = Row::from(vec![ if let Some(k) = single_order.kind { match k { - Kind::Buy => Cell::new(k.to_string()) + mostro_core::order::Kind::Buy => Cell::new(k.to_string()) .fg(Color::Green) .set_alignment(CellAlignment::Center), - Kind::Sell => Cell::new(k.to_string()) + mostro_core::order::Kind::Sell => Cell::new(k.to_string()) .fg(Color::Red) .set_alignment(CellAlignment::Center), } @@ -155,10 +227,10 @@ pub fn print_orders_table(orders_table: Vec) -> Result { let r = Row::from(vec![ if let Some(k) = single_order.kind { match k { - Kind::Buy => Cell::new(k.to_string()) + mostro_core::order::Kind::Buy => Cell::new(k.to_string()) .fg(Color::Green) .set_alignment(CellAlignment::Center), - Kind::Sell => Cell::new(k.to_string()) + mostro_core::order::Kind::Sell => Cell::new(k.to_string()) .fg(Color::Red) .set_alignment(CellAlignment::Center), } @@ -198,76 +270,3 @@ pub fn print_orders_table(orders_table: Vec) -> Result { Ok(table.to_string()) } - -pub fn print_disputes_table(disputes_table: Vec) -> Result { - // Convert Event to Dispute - let disputes_table: Vec = disputes_table - .into_iter() - .filter_map(|event| { - if let Event::Dispute(dispute) = event { - Some(dispute) - } else { - None - } - }) - .collect(); - - // Create table - let mut table = Table::new(); - //Table rows - let mut rows: Vec = Vec::new(); - - if disputes_table.is_empty() { - table - .load_preset(UTF8_FULL) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_width(160) - .set_header(vec![Cell::new("Sorry...") - .add_attribute(Attribute::Bold) - .set_alignment(CellAlignment::Center)]); - - // Single row for error - let mut r = Row::new(); - - r.add_cell( - Cell::new("No disputes found with requested parameters...") - .fg(Color::Red) - .set_alignment(CellAlignment::Center), - ); - - //Push single error row - rows.push(r); - } else { - table - .load_preset(UTF8_FULL) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_width(160) - .set_header(vec![ - Cell::new("Dispute Id") - .add_attribute(Attribute::Bold) - .set_alignment(CellAlignment::Center), - Cell::new("Status") - .add_attribute(Attribute::Bold) - .set_alignment(CellAlignment::Center), - Cell::new("Created") - .add_attribute(Attribute::Bold) - .set_alignment(CellAlignment::Center), - ]); - - //Iterate to create table of orders - for single_dispute in disputes_table.into_iter() { - let date = DateTime::from_timestamp(single_dispute.created_at, 0); - - let r = Row::from(vec![ - Cell::new(single_dispute.id).set_alignment(CellAlignment::Center), - Cell::new(single_dispute.status.to_string()).set_alignment(CellAlignment::Center), - Cell::new(date.unwrap()), - ]); - rows.push(r); - } - } - - table.add_rows(rows); - - Ok(table.to_string()) -} diff --git a/src/util.rs b/src/util.rs index d2d4f6b..7319a0d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,22 +1,24 @@ +use crate::cli::send_msg::execute_send_msg; +use crate::cli::Commands; use crate::db::{connect, Order, User}; -use crate::nip33::{dispute_from_tags, order_from_tags}; +use crate::parser::{parse_dispute_events, parse_dm_events, parse_orders_events}; use anyhow::{Error, Result}; use base64::engine::general_purpose; use base64::Engine; use dotenvy::var; -use log::{error, info}; use mostro_core::prelude::*; -use nip44::v2::{decrypt_to_bytes, encrypt_to_bytes, ConversationKey}; +use nip44::v2::{encrypt_to_bytes, ConversationKey}; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; use std::time::Duration; use std::{fs, path::Path}; -use crate::cli::{MOSTRO_KEYS, POOL, TRADE_KEY}; +use uuid::Uuid; #[derive(Clone, Debug)] pub enum Event { SmallOrder(SmallOrder), Dispute(Dispute), // Assuming you have a Dispute struct - MessageTuple((Message, u64)), + MessageTuple(Box<(Message, u64)>), } #[derive(Clone, Debug)] @@ -237,6 +239,91 @@ pub async fn wait_for_dm( } } +#[derive(Debug, Clone, Copy)] +enum MessageType { + PrivateDirectMessage, + PrivateGiftWrap, + SignedGiftWrap, +} + +fn determine_message_type(to_user: bool, private: bool) -> MessageType { + match (to_user, private) { + (true, _) => MessageType::PrivateDirectMessage, + (false, true) => MessageType::PrivateGiftWrap, + (false, false) => MessageType::SignedGiftWrap, + } +} + +fn create_expiration_tags(expiration: Option) -> Tags { + let mut tags: Vec = Vec::with_capacity(1 + usize::from(expiration.is_some())); + + if let Some(timestamp) = expiration { + tags.push(Tag::expiration(timestamp)); + } + + Tags::from_list(tags) +} + +async fn create_private_dm_event( + trade_keys: &Keys, + receiver_pubkey: &PublicKey, + payload: String, + pow: u8, +) -> Result { + // Derive conversation key + let ck = ConversationKey::derive(trade_keys.secret_key(), receiver_pubkey)?; + // Encrypt payload + let encrypted_content = encrypt_to_bytes(&ck, payload.as_bytes())?; + // Encode with base64 + let b64decoded_content = general_purpose::STANDARD.encode(encrypted_content); + // Compose builder + Ok( + EventBuilder::new(nostr_sdk::Kind::PrivateDirectMessage, b64decoded_content) + .pow(pow) + .tag(Tag::public_key(*receiver_pubkey)) + .sign_with_keys(trade_keys)?, + ) +} + +async fn create_gift_wrap_event( + trade_keys: &Keys, + identity_keys: Option<&Keys>, + receiver_pubkey: &PublicKey, + payload: String, + pow: u8, + expiration: Option, + signed: bool, +) -> Result { + let message = Message::from_json(&payload).unwrap(); + + let content = if signed { + let _identity_keys = identity_keys + .ok_or_else(|| Error::msg("identity_keys required for signed messages"))?; + // We sign the message + let sig = Message::sign(payload, trade_keys); + serde_json::to_string(&(message, sig)).unwrap() + } else { + // We compose the content, when private we don't sign the payload + let content: (Message, Option) = (message, None); + serde_json::to_string(&content).unwrap() + }; + + // We create the rumor + let rumor = EventBuilder::text_note(content) + .pow(pow) + .build(trade_keys.public_key()); + + let tags = create_expiration_tags(expiration); + + let signer_keys = if signed { + identity_keys.unwrap() + } else { + trade_keys + }; + + Ok(EventBuilder::gift_wrap(signer_keys, receiver_pubkey, rumor, tags).await?) +} + pub async fn send_dm( client: &Client, identity_keys: Option<&Keys>, @@ -251,59 +338,40 @@ pub async fn send_dm( .unwrap_or("false".to_string()) .parse::() .unwrap(); - let event = if to_user { - // Derive conversation key - let ck = ConversationKey::derive(trade_keys.secret_key(), receiver_pubkey)?; - // Encrypt payload - let encrypted_content = encrypt_to_bytes(&ck, payload.as_bytes())?; - // Encode with base64 - let b64decoded_content = general_purpose::STANDARD.encode(encrypted_content); - // Compose builder - EventBuilder::new(nostr_sdk::Kind::PrivateDirectMessage, b64decoded_content) - .pow(pow) - .tag(Tag::public_key(*receiver_pubkey)) - .sign_with_keys(trade_keys)? - } else if private { - let message = Message::from_json(&payload).unwrap(); - // We compose the content, when private we don't sign the payload - let content: (Message, Option) = (message, None); - let content = serde_json::to_string(&content).unwrap(); - // We create the rumor - let rumor = EventBuilder::text_note(content) - .pow(pow) - .build(trade_keys.public_key()); - let mut tags: Vec = Vec::with_capacity(1 + usize::from(expiration.is_some())); - if let Some(timestamp) = expiration { - tags.push(Tag::expiration(timestamp)); - } - let tags = Tags::from_list(tags); - - EventBuilder::gift_wrap(trade_keys, receiver_pubkey, rumor, tags).await? - } else { - let identity_keys = identity_keys - .ok_or_else(|| Error::msg("identity_keys required when to_user is false"))?; - // We sign the message - let message = Message::from_json(&payload).unwrap(); - let sig = Message::sign(payload.clone(), trade_keys); - // We compose the content - let content = serde_json::to_string(&(message, sig)).unwrap(); - // We create the rumor - let rumor = EventBuilder::text_note(content) - .pow(pow) - .build(trade_keys.public_key()); - let mut tags: Vec = Vec::with_capacity(1 + usize::from(expiration.is_some())); + let message_type = determine_message_type(to_user, private); - if let Some(timestamp) = expiration { - tags.push(Tag::expiration(timestamp)); + let event = match message_type { + MessageType::PrivateDirectMessage => { + create_private_dm_event(trade_keys, receiver_pubkey, payload, pow).await? + } + MessageType::PrivateGiftWrap => { + create_gift_wrap_event( + trade_keys, + identity_keys, + receiver_pubkey, + payload, + pow, + expiration, + false, + ) + .await? + } + MessageType::SignedGiftWrap => { + create_gift_wrap_event( + trade_keys, + identity_keys, + receiver_pubkey, + payload, + pow, + expiration, + true, + ) + .await? } - let tags = Tags::from_list(tags); - - EventBuilder::gift_wrap(identity_keys, receiver_pubkey, rumor, tags).await? }; client.send_event(&event).await?; - Ok(()) } @@ -387,259 +455,7 @@ pub async fn get_direct_messages_from_trade_keys( Ok(()) } -pub async fn get_direct_messages( - client: &Client, - my_key: &Keys, - since: i64, - from_user: bool, - mostro_pubkey: Option<&PublicKey>, -) -> Vec<(Message, u64)> { - // We use a fake timestamp to thwart time-analysis attacks - let fake_since = 2880; - let fake_since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(fake_since)) - .unwrap() - .timestamp() as u64; - - let fake_timestamp = Timestamp::from(fake_since_time); - let filters = if from_user { - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(since)) - .unwrap() - .timestamp() as u64; - let timestamp = Timestamp::from(since_time); - Filter::new() - .kind(nostr_sdk::Kind::PrivateDirectMessage) - .pubkey(my_key.public_key()) - .since(timestamp) - } else { - Filter::new() - .kind(nostr_sdk::Kind::GiftWrap) - .pubkey(my_key.public_key()) - .since(fake_timestamp) - }; - - info!("Request events with event kind : {:?} ", filters.kinds); - - let mut direct_messages: Vec<(Message, u64)> = Vec::new(); - - if let Ok(mostro_req) = client.fetch_events(filters, Duration::from_secs(15)).await { - // Buffer vector for direct messages - // Vector for single order id check - maybe multiple relay could send the same order id? Check unique one... - let mut id_list = Vec::::new(); - - for dm in mostro_req.iter() { - if !id_list.contains(&dm.id) { - id_list.push(dm.id); - let (created_at, message) = if from_user { - let ck = - if let Ok(ck) = ConversationKey::derive(my_key.secret_key(), &dm.pubkey) { - ck - } else { - continue; - }; - let b64decoded_content = - match general_purpose::STANDARD.decode(dm.content.as_bytes()) { - Ok(b64decoded_content) => b64decoded_content, - Err(_) => { - continue; - } - }; - - let unencrypted_content = decrypt_to_bytes(&ck, &b64decoded_content) - .expect("Failed to decrypt message"); - - let message = - String::from_utf8(unencrypted_content).expect("Found invalid UTF-8"); - let message = Message::from_json(&message).expect("Failed on deserializing"); - - (dm.created_at, message) - } else { - let unwrapped_gift = match nip59::extract_rumor(my_key, dm).await { - Ok(u) => u, - Err(_) => { - println!("Error unwrapping gift"); - continue; - } - }; - - // Filter: only process messages from Mostro - if let Some(mostro_pk) = mostro_pubkey { - if unwrapped_gift.rumor.pubkey != *mostro_pk { - continue; // Skip non-Mostro messages - } - } - - let (message, _): (Message, Option) = - serde_json::from_str(&unwrapped_gift.rumor.content).unwrap(); - - (unwrapped_gift.rumor.created_at, message) - }; - - // Here we discard messages older than the real since parameter - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(30)) - .unwrap() - .timestamp() as u64; - if created_at.as_u64() < since_time { - continue; - } - direct_messages.push((message, created_at.as_u64())); - } - } - // Return element sorted by second tuple element ( Timestamp ) - direct_messages.sort_by(|a, b| a.1.cmp(&b.1)); - } - - direct_messages -} - -fn parse_dispute_events(events: Events) -> Vec { - // Extracted Disputes List - let mut disputes_list = Vec::::new(); - - // Scan events to extract all disputes - for event in events.into_iter() { - if let Ok(mut dispute) = dispute_from_tags(event.tags) { - info!("Found Dispute id : {:?}", dispute.id); - // Get created at field from Nostr event - dispute.created_at = event.created_at.as_u64() as i64; - disputes_list.push(dispute.clone()); - } - } - - let buffer_dispute_list = disputes_list.clone(); - // Order all element ( orders ) received to filter - discard disaligned messages - // if an order has an older message with the state we received is discarded for the latest one - disputes_list.retain(|keep| { - !buffer_dispute_list - .iter() - .any(|x| x.id == keep.id && x.created_at > keep.created_at) - }); - - // Sort by id to remove duplicates - disputes_list.sort_by(|a, b| b.id.cmp(&a.id)); - disputes_list.dedup_by(|a, b| a.id == b.id); - - // Finally sort list by creation time - disputes_list.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - disputes_list -} - - -async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message,u64)>{ - // Buffer vector for direct messages - // Vector for single order id check - maybe multiple relay could send the same order id? Check unique one... - let mut id_list = Vec::::new(); - // Vector for direct messages - let mut direct_messages: Vec<(Message, u64)> = Vec::new(); - - for dm in events.iter() { - if !id_list.contains(&dm.id) { - id_list.push(dm.id); - - let unwrapped_gift = match nip59::extract_rumor(pubkey, dm).await { - Ok(u) => u, - Err(_) => { - println!("Error unwrapping gift"); - continue; - } - }; - let (message, _): (Message, Option) = - serde_json::from_str(&unwrapped_gift.rumor.content).unwrap(); - - // Create a tuple with the created_at and the message - let (created_at, message) = (unwrapped_gift.rumor.created_at, message); - - - // Here we discard messages older than the real since parameter - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(30)) - .unwrap() - .timestamp() as u64; - if created_at.as_u64() < since_time { - continue; - } - direct_messages.push((message, created_at.as_u64())); - } - } - // Return element sorted by second tuple element ( Timestamp ) - direct_messages.sort_by(|a, b| a.1.cmp(&b.1)); - - direct_messages -} - -fn parse_orders_events( - events: Events, - currency: Option, - status: Option, - kind: Option, -) -> Vec { - // Extracted Orders List - let mut complete_events_list = Vec::::new(); - let mut requested_orders_list = Vec::::new(); - - // Scan events to extract all orders - for event in events.iter() { - let order = order_from_tags(event.tags.clone()); - - if order.is_err() { - error!("{order:?}"); - continue; - } - if let Ok(mut order) = order { - - info!("Found Order id : {:?}", order.id.unwrap()); - - if order.id.is_none() { - info!("Order ID is none"); - continue; - } - - if order.kind.is_none() { - info!("Order kind is none"); - continue; - } - - if order.status.is_none() { - info!("Order status is none"); - continue; - } - - // Get created at field from Nostr event - order.created_at = Some(event.created_at.as_u64() as i64); - complete_events_list.push(order.clone()); - if order.status.ne(&status) { - continue; - } - - if currency.is_some() && order.fiat_code.ne(¤cy.clone().unwrap()) { - continue; - } - - if kind.is_some() && order.kind.ne(&kind) { - continue; - } - // Add just requested orders requested by filtering - requested_orders_list.push(order); - } - // Order all element ( orders ) received to filter - discard disaligned messages - // if an order has an older message with the state we received is discarded for the latest one - requested_orders_list.retain(|keep| { - !complete_events_list - .iter() - .any(|x| x.id == keep.id && x.created_at > keep.created_at) - }); - // Sort by id to remove duplicates - requested_orders_list.sort_by(|a, b| b.id.cmp(&a.id)); - requested_orders_list.dedup_by(|a, b| a.id == b.id); - } - // Finally sort list by creation time - requested_orders_list.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - requested_orders_list -} - -fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { +pub fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { match list_kind { ListKind::Orders => { let since_time = chrono::Utc::now() @@ -693,120 +509,77 @@ fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { } ListKind::DirectMessagesUser => { let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(30)) - .unwrap() - .timestamp() as u64; - let timestamp = Timestamp::from(since_time); - Filter::new() - .kind(nostr_sdk::Kind::PrivateDirectMessage) - .pubkey(pubkey) - .since(timestamp) + .checked_sub_signed(chrono::Duration::minutes(30)) + .unwrap() + .timestamp() as u64; + let timestamp = Timestamp::from(since_time); + Filter::new() + .kind(nostr_sdk::Kind::PrivateDirectMessage) + .pubkey(pubkey) + .since(timestamp) } } } - +#[allow(clippy::too_many_arguments)] pub async fn fetch_events_list( list_kind: ListKind, status: Option, currency: Option, kind: Option, + mostro_pubkey: PublicKey, + mostro_keys: &Keys, + trade_index: i64, + pool: &SqlitePool, client: &Client, ) -> Result> { match list_kind { ListKind::Orders => { - let filters = create_filter(list_kind, MOSTRO_KEYS.get().unwrap().public_key()); - let fetched_events = client.fetch_events(filters, Duration::from_secs(15)).await?; + let filters = create_filter(list_kind, mostro_pubkey); + let fetched_events = client + .fetch_events(filters, Duration::from_secs(15)) + .await?; let orders = parse_orders_events(fetched_events, currency, status, kind); Ok(orders.into_iter().map(Event::SmallOrder).collect()) } ListKind::DirectMessagesAdmin => { - let filters = create_filter(list_kind, MOSTRO_KEYS.get().unwrap().public_key()); - let fetched_events = client.fetch_events(filters, Duration::from_secs(15)).await?; - let direct_messages_mostro = parse_dm_events(fetched_events, MOSTRO_KEYS.get().unwrap()).await; - Ok(direct_messages_mostro.into_iter().map(Event::MessageTuple).collect()) + let filters = create_filter(list_kind, mostro_keys.public_key()); + let fetched_events = client + .fetch_events(filters, Duration::from_secs(15)) + .await?; + let direct_messages_mostro = parse_dm_events(fetched_events, mostro_keys).await; + Ok(direct_messages_mostro + .into_iter() + .map(|t| Event::MessageTuple(Box::new(t))) + .collect()) } ListKind::DirectMessagesUser => { - let trade_index = TRADE_KEY.get().unwrap().1; let mut direct_messages: Vec<(Message, u64)> = Vec::new(); for index in 1..=trade_index { - let trade_key = User::get_trade_keys(&POOL.get().unwrap(), index).await?; + let trade_key = User::get_trade_keys(pool, index).await?; let filter = create_filter(ListKind::DirectMessagesUser, trade_key.public_key()); - let fetched_user_messages = client.fetch_events(filter, Duration::from_secs(15)).await?; - let direct_messages_for_trade_key = parse_dm_events(fetched_user_messages, &trade_key).await; + let fetched_user_messages = + client.fetch_events(filter, Duration::from_secs(15)).await?; + let direct_messages_for_trade_key = + parse_dm_events(fetched_user_messages, &trade_key).await; direct_messages.extend(direct_messages_for_trade_key); } - Ok(direct_messages.into_iter().map(Event::MessageTuple).collect()) - }, + Ok(direct_messages + .into_iter() + .map(|t| Event::MessageTuple(Box::new(t))) + .collect()) + } ListKind::Disputes => { - let filters = create_filter(list_kind, MOSTRO_KEYS.get().unwrap().public_key()); - let fetched_events = client.fetch_events(filters, Duration::from_secs(15)).await?; + let filters = create_filter(list_kind, mostro_pubkey); + let fetched_events = client + .fetch_events(filters, Duration::from_secs(15)) + .await?; let disputes = parse_dispute_events(fetched_events); Ok(disputes.into_iter().map(Event::Dispute).collect()) } } } -pub async fn get_disputes_list(pubkey: PublicKey, client: &Client) -> Result> { - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::days(7)) - .unwrap() - .timestamp() as u64; - - let timestamp = Timestamp::from(since_time); - - let filter = Filter::new() - .author(pubkey) - .limit(50) - .since(timestamp) - .custom_tag( - SingleLetterTag::lowercase(Alphabet::Z), - "dispute".to_string(), - ) - .kind(nostr_sdk::Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND)); - - // Extracted Orders List - let mut disputes_list = Vec::::new(); - - // Send all requests to relays - if let Ok(mostro_req) = client.fetch_events(filter, Duration::from_secs(15)).await { - // Scan events to extract all disputes - for d in mostro_req.iter() { - let dispute = dispute_from_tags(d.tags.clone()); - - if dispute.is_err() { - error!("{dispute:?}"); - continue; - } - let mut dispute = dispute?; - - info!("Found Dispute id : {:?}", dispute.id); - - // Get created at field from Nostr event - dispute.created_at = d.created_at.as_u64() as i64; - disputes_list.push(dispute); - } - } - - let buffer_dispute_list = disputes_list.clone(); - // Order all element ( orders ) received to filter - discard disaligned messages - // if an order has an older message with the state we received is discarded for the latest one - disputes_list.retain(|keep| { - !buffer_dispute_list - .iter() - .any(|x| x.id == keep.id && x.created_at > keep.created_at) - }); - - // Sort by id to remove duplicates - disputes_list.sort_by(|a, b| b.id.cmp(&a.id)); - disputes_list.dedup_by(|a, b| a.id == b.id); - - // Finally sort list by creation time - disputes_list.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - - Ok(disputes_list) -} - /// Uppercase first letter of a string. pub fn uppercase_first(s: &str) -> String { let mut c = s.chars(); @@ -826,3 +599,21 @@ pub fn get_mcli_path() -> String { mcli_path } + +pub async fn run_simple_order_msg( + command: Commands, + order_id: &Uuid, + identity_keys: &Keys, + mostro_key: PublicKey, + client: &Client, +) -> Result<()> { + execute_send_msg( + command, + Some(*order_id), + Some(identity_keys), + mostro_key, + client, + None, + ) + .await +} From e2db8624903d463d999ae21261251ce6072677e7 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Mon, 8 Sep 2025 23:46:26 +0200 Subject: [PATCH 03/19] feat: successfully rebased on main branch the cli refactor --- Cargo.lock | 4 +- Cargo.toml | 2 +- src/cli.rs | 173 ++++++++++++++++++++++------------------- src/cli/get_dm.rs | 2 +- src/cli/get_dm_user.rs | 26 ++++--- src/util.rs | 52 ++++++++----- 6 files changed, 146 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 541e1fe..02cc78c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1740,9 +1740,9 @@ dependencies = [ [[package]] name = "mostro-core" -version = "0.6.49" +version = "0.6.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628171bedc33fbc0b5b7c783566047541954155776ff2e41cbbea517657cf9c5" +checksum = "c8bf32904269e30059c5354a3f379a90ebb6afa7355995d624ec6403062ccd47" dependencies = [ "argon2", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 37d52ea..e9b6884 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ uuid = { version = "1.18.1", features = [ dotenvy = "0.15.6" lightning-invoice = { version = "0.33.2", features = ["std"] } reqwest = { version = "0.12.23", features = ["json"] } -mostro-core = "0.6.49" +mostro-core = "0.6.50" lnurl-rs = "0.9.0" pretty_env_logger = "0.5.0" openssl = { version = "0.10.73", features = ["vendored"] } diff --git a/src/cli.rs b/src/cli.rs index 4427a38..1f4e459 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -424,9 +424,45 @@ async fn init_context(cli: &Cli) -> Result { impl Commands { pub async fn run(&self, ctx: &Context) -> Result<()> { match self { + // Simple order message commands + Commands::FiatSent { order_id } + | Commands::Release { order_id } + | Commands::Dispute { order_id } + | Commands::Cancel { order_id } => { + crate::util::run_simple_order_msg( + self.clone(), + order_id, + &ctx.identity_keys, + ctx.mostro_pubkey, + &ctx.client, + ) + .await + } + + // DM commands with pubkey parsing + Commands::SendDm { + pubkey, + order_id, + message, + } => { + execute_send_dm(PublicKey::from_str(pubkey)?, &ctx.client, order_id, message).await + } + Commands::DmToUser { + pubkey, + order_id, + message, + } => { + execute_dm_to_user(PublicKey::from_str(pubkey)?, &ctx.client, order_id, message) + .await + } + Commands::AdmSendDm { pubkey, message } => { + execute_adm_send_dm(PublicKey::from_str(pubkey)?, &ctx.client, message).await + } Commands::ConversationKey { pubkey } => { execute_conversation_key(&ctx.trade_keys, PublicKey::from_str(pubkey)?).await } + + // Order management commands Commands::ListOrders { status, currency, @@ -444,6 +480,33 @@ impl Commands { ) .await } + Commands::NewOrder { + kind, + fiat_code, + amount, + fiat_amount, + payment_method, + premium, + invoice, + expiration_days, + } => { + execute_new_order( + kind, + fiat_code, + fiat_amount, + amount, + payment_method, + premium, + invoice, + &ctx.identity_keys, + &ctx.trade_keys, + ctx.trade_index, + ctx.mostro_pubkey, + &ctx.client, + expiration_days, + ) + .await + } Commands::TakeSell { order_id, invoice, @@ -483,6 +546,18 @@ impl Commands { ) .await } + Commands::Rate { order_id, rating } => { + execute_rate_user( + order_id, + rating, + &ctx.identity_keys, + ctx.mostro_pubkey, + &ctx.client, + ) + .await + } + + // DM retrieval commands Commands::GetDm { since, from_user } => { execute_get_dm( since, @@ -490,13 +565,14 @@ impl Commands { &ctx.mostro_keys, &ctx.client, *from_user, - false, &mostro_key).await? - } - Commands::GetDmUser { since } => { - execute_get_dm_user(since, &client, &mostro_key, + false, + &ctx.mostro_pubkey, ) .await } + Commands::GetDmUser { since } => { + execute_get_dm_user(since, &ctx.client, &ctx.mostro_pubkey).await + } Commands::GetAdminDm { since, from_user } => { execute_get_dm( since, @@ -505,18 +581,18 @@ impl Commands { &ctx.client, *from_user, true, + &ctx.mostro_pubkey, ) .await } - Commands::FiatSent { order_id } - | Commands::Release { order_id } - | Commands::Dispute { order_id } - | Commands::Cancel { order_id } => { - crate::util::run_simple_order_msg( - self.clone(), - order_id, - &ctx.identity_keys, + + // Admin commands + Commands::AdmListDisputes {} => { + execute_list_disputes( ctx.mostro_pubkey, + &ctx.mostro_keys, + ctx.trade_index, + &ctx.pool, &ctx.client, ) .await @@ -531,46 +607,6 @@ impl Commands { ) .await } - Commands::NewOrder { - kind, - fiat_code, - amount, - fiat_amount, - payment_method, - premium, - invoice, - expiration_days, - } => { - execute_new_order( - kind, - fiat_code, - fiat_amount, - amount, - payment_method, - premium, - invoice, - &ctx.identity_keys, - &ctx.trade_keys, - ctx.trade_index, - ctx.mostro_pubkey, - &ctx.client, - expiration_days, - ) - .await - } - Commands::Rate { order_id, rating } => { - execute_rate_user( - order_id, - rating, - &ctx.identity_keys, - ctx.mostro_pubkey, - &ctx.client, - ) - .await - } - Commands::Restore {} => { - execute_restore(&identity_keys, mostro_key, &client).await?; - } Commands::AdmSettle { order_id } => { execute_admin_settle_dispute( order_id, @@ -601,35 +637,10 @@ impl Commands { ) .await } - Commands::AdmListDisputes {} => { - execute_list_disputes( - ctx.mostro_pubkey, - &ctx.mostro_keys, - ctx.trade_index, - &ctx.pool, - &ctx.client, - ) - .await - } - Commands::SendDm { - pubkey, - order_id, - message, - } => { - let pubkey = PublicKey::from_str(pubkey)?; - execute_send_dm(pubkey, &ctx.client, order_id, message).await - } - Commands::DmToUser { - pubkey, - order_id, - message, - } => { - let pubkey = PublicKey::from_str(pubkey)?; - execute_dm_to_user(pubkey, &client, order_id, message).await? - } - Commands::AdmSendDm { pubkey, message } => { - let pubkey = PublicKey::from_str(pubkey)?; - execute_adm_send_dm(pubkey, &client, message).await? + + // Simple commands + Commands::Restore {} => { + execute_restore(&ctx.identity_keys, ctx.mostro_pubkey, &ctx.client).await } } } diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index 031a3cf..0cf10dd 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -16,7 +16,7 @@ pub async fn execute_get_dm( client: &Client, _from_user: bool, admin: bool, - mostro_pubkey: &PublicKey, + _mostro_pubkey: &PublicKey, ) -> Result<()> { let mut dm: Vec<(Message, u64)> = Vec::new(); let pool = connect().await?; diff --git a/src/cli/get_dm_user.rs b/src/cli/get_dm_user.rs index de7a64f..d5720a0 100644 --- a/src/cli/get_dm_user.rs +++ b/src/cli/get_dm_user.rs @@ -6,25 +6,33 @@ use comfy_table::Table; use mostro_core::prelude::*; use nostr_sdk::prelude::*; -pub async fn execute_get_dm_user(since: &i64, client: &Client, mostro_pubkey: &PublicKey) -> Result<()> { +pub async fn execute_get_dm_user( + since: &i64, + client: &Client, + mostro_pubkey: &PublicKey, +) -> Result<()> { let pool = crate::db::connect().await?; - + // Get all trade keys from orders let mut trade_keys_hex = Order::get_all_trade_keys(&pool).await?; - + // Add admin private key to search for messages sent TO admin if let Ok(admin_privkey_hex) = std::env::var("NSEC_PRIVKEY") { trade_keys_hex.push(admin_privkey_hex); } - + if trade_keys_hex.is_empty() { println!("No trade keys found in orders and NSEC_PRIVKEY not set"); return Ok(()); } - - println!("Searching for DMs in {} trade keys...", trade_keys_hex.len()); - - let direct_messages = get_direct_messages_from_trade_keys(client, trade_keys_hex, *since, mostro_pubkey).await; + + println!( + "Searching for DMs in {} trade keys...", + trade_keys_hex.len() + ); + + let direct_messages = + get_direct_messages_from_trade_keys(client, trade_keys_hex, *since, mostro_pubkey).await?; if direct_messages.is_empty() { println!("You don't have any direct messages in your trade keys"); @@ -59,4 +67,4 @@ pub async fn execute_get_dm_user(since: &i64, client: &Client, mostro_pubkey: &P println!("{table}"); println!(); Ok(()) -} \ No newline at end of file +} diff --git a/src/util.rs b/src/util.rs index 7319a0d..2286773 100644 --- a/src/util.rs +++ b/src/util.rs @@ -6,6 +6,7 @@ use anyhow::{Error, Result}; use base64::engine::general_purpose; use base64::Engine; use dotenvy::var; +use log::info; use mostro_core::prelude::*; use nip44::v2::{encrypt_to_bytes, ConversationKey}; use nostr_sdk::prelude::*; @@ -40,7 +41,7 @@ async fn send_gift_wrap_dm_internal( .unwrap_or_else(|_| "0".to_string()) .parse() .unwrap_or(0); - + // Create Message struct for consistency with Mostro protocol let dm_message = Message::new_dm( None, @@ -48,22 +49,25 @@ async fn send_gift_wrap_dm_internal( Action::SendDm, Some(Payload::TextMessage(message.to_string())), ); - + // Serialize as JSON with the expected format (Message, Option) let content = serde_json::to_string(&(dm_message, None::))?; - + // Create the rumor with JSON content let rumor = EventBuilder::text_note(content) .pow(pow) .build(sender_keys.public_key()); - + // Create gift wrap using sender_keys as the signing key let event = EventBuilder::gift_wrap(sender_keys, receiver_pubkey, rumor, Tags::new()).await?; - + let sender_type = if is_admin { "admin" } else { "user" }; - info!("Sending {} gift wrap event to {}", sender_type, receiver_pubkey); + info!( + "Sending {} gift wrap event to {}", + sender_type, receiver_pubkey + ); client.send_event(&event).await?; - + Ok(()) } @@ -428,10 +432,10 @@ pub async fn get_direct_messages_from_trade_keys( client: &Client, trade_keys_hex: Vec, since: i64, - mostro_pubkey: &PublicKey, -) -> Vec<(Message, u64, PublicKey)> { + _mostro_pubkey: &PublicKey, +) -> Result> { if trade_keys_hex.is_empty() { - return Vec::new(); + return Ok(Vec::new()); } let fake_since = 2880; @@ -439,20 +443,30 @@ pub async fn get_direct_messages_from_trade_keys( .checked_sub_signed(chrono::Duration::minutes(fake_since)) .unwrap() .timestamp() as u64; - let fake_timestamp = Timestamp::from(fake_since_time); - - let since_time = chrono::Utc::now() + let _fake_timestamp = Timestamp::from(fake_since_time); + + let _since_time = chrono::Utc::now() .checked_sub_signed(chrono::Duration::minutes(since)) .unwrap() .timestamp() as u64; - let dm: Vec<(Message, u64)> = if wait_for_dm { - get_direct_messages(client, trade_keys, 15, to_user).await - } else { - Vec::new() - }; + let mut all_messages: Vec<(Message, u64, PublicKey)> = Vec::new(); + + for trade_key_hex in trade_keys_hex { + if let Ok(public_key) = PublicKey::from_hex(&trade_key_hex) { + let filter = create_filter(ListKind::DirectMessagesUser, public_key); + let events = client.fetch_events(filter, Duration::from_secs(15)).await?; + // Parse events without keys since we only have the public key + // We'll need to handle this differently - let's just collect the events for now + for event in events { + if let Ok(message) = Message::from_json(&event.content) { + all_messages.push((message, event.created_at.as_u64(), public_key)); + } + } + } + } - Ok(()) + Ok(all_messages) } pub fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { From 6451dd90e3fbd816cbbc3a5bc4a03e74f3721c65 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Tue, 9 Sep 2025 23:30:35 +0200 Subject: [PATCH 04/19] refactor: completing new commands refactor, neworder and take order works now --- src/cli.rs | 35 ++----- src/cli/add_invoice.rs | 47 ++++------ src/cli/adm_send_dm.rs | 7 +- src/cli/dm_to_user.rs | 7 +- src/cli/get_dm.rs | 84 +---------------- src/cli/get_dm_user.rs | 6 +- src/cli/new_order.rs | 2 +- src/cli/send_msg.rs | 2 +- src/cli/take_buy.rs | 2 +- src/cli/take_sell.rs | 2 +- src/parser/dms.rs | 202 +++++++++++++++++++++++++++++------------ src/util.rs | 16 +++- 12 files changed, 204 insertions(+), 208 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 1f4e459..bc59a1b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -177,9 +177,6 @@ pub enum Commands { #[arg(short, long)] #[clap(default_value_t = 30)] since: i64, - /// If true, get messages from counterparty, otherwise from Mostro - #[arg(short)] - from_user: bool, }, /// Get direct messages sent to any trade keys GetDmUser { @@ -194,9 +191,6 @@ pub enum Commands { #[arg(short, long)] #[clap(default_value_t = 30)] since: i64, - /// If true, get messages from counterparty, otherwise from Mostro - #[arg(short)] - from_user: bool, }, /// Send direct message to a user SendDm { @@ -543,6 +537,7 @@ impl Commands { &ctx.identity_keys, ctx.mostro_pubkey, &ctx.client, + &ctx.pool, ) .await } @@ -558,32 +553,14 @@ impl Commands { } // DM retrieval commands - Commands::GetDm { since, from_user } => { - execute_get_dm( - since, - ctx.trade_index, - &ctx.mostro_keys, - &ctx.client, - *from_user, - false, - &ctx.mostro_pubkey, - ) - .await + Commands::GetDm { since } => { + execute_get_dm(since, ctx.trade_index, &ctx.mostro_keys, &ctx.client, false).await } Commands::GetDmUser { since } => { - execute_get_dm_user(since, &ctx.client, &ctx.mostro_pubkey).await + execute_get_dm_user(since, &ctx.client, &ctx.mostro_pubkey, &ctx.pool).await } - Commands::GetAdminDm { since, from_user } => { - execute_get_dm( - since, - ctx.trade_index, - &ctx.mostro_keys, - &ctx.client, - *from_user, - true, - &ctx.mostro_pubkey, - ) - .await + Commands::GetAdminDm { since } => { + execute_get_dm(since, ctx.trade_index, &ctx.mostro_keys, &ctx.client, true).await } // Admin commands diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index cbbe912..ae1ddae 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -1,10 +1,10 @@ -use crate::db::connect; use crate::util::{send_dm, wait_for_dm}; use crate::{db::Order, lightning::is_valid_invoice}; use anyhow::Result; use lnurl::lightning_address::LightningAddress; use mostro_core::prelude::*; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; use std::str::FromStr; use uuid::Uuid; @@ -14,9 +14,9 @@ pub async fn execute_add_invoice( identity_keys: &Keys, mostro_key: PublicKey, client: &Client, + pool: &SqlitePool, ) -> Result<()> { - let pool = connect().await?; - let order = Order::get_by_id(&pool, &order_id.to_string()).await?; + let order = Order::get_by_id(pool, &order_id.to_string()).await?; let trade_keys = order .trade_keys .clone() @@ -40,6 +40,8 @@ pub async fn execute_add_invoice( } } }; + + // Create request id let request_id = Uuid::new_v4().as_u128() as u64; // Create AddInvoice message let add_invoice_message = Message::new_order( @@ -50,35 +52,24 @@ pub async fn execute_add_invoice( payload, ); + // Serialize the message let message_json = add_invoice_message .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - // Clone the keys and client for the async call - let identity_keys = identity_keys.clone(); - let trade_keys_clone = trade_keys.clone(); - let client_clone = client.clone(); - - // Spawn a new task to send the DM - // This is so we can wait for the gift wrap event in the main thread - tokio::spawn(async move { - if let Err(e) = send_dm( - &client_clone, - Some(&identity_keys.clone()), - &trade_keys_clone, - &mostro_key, - message_json, - None, - false, - ) - .await - { - eprintln!("Failed to send DM: {}", e); - } - }); - - // Wait for the DM to be sent from mostro - wait_for_dm(client, &trade_keys, request_id, 0, Some(order)).await?; + // Send DM + send_dm( + client, + Some(identity_keys), + &trade_keys, + &mostro_key, + message_json, + None, + false, + ) + .await?; + // Wait for the DM to be sent from mostro and update the order + wait_for_dm(client, &trade_keys, request_id, None, Some(order)).await?; Ok(()) } diff --git a/src/cli/adm_send_dm.rs b/src/cli/adm_send_dm.rs index f8d0566..f4993dc 100644 --- a/src/cli/adm_send_dm.rs +++ b/src/cli/adm_send_dm.rs @@ -14,11 +14,14 @@ pub async fn execute_adm_send_dm( } }; - println!("SENDING DM with admin keys: {}", admin_keys.public_key().to_hex()); + println!( + "SENDING DM with admin keys: {}", + admin_keys.public_key().to_hex() + ); send_admin_gift_wrap_dm(client, &admin_keys, &receiver, message).await?; println!("Admin gift wrap message sent to {}", receiver); Ok(()) -} \ No newline at end of file +} diff --git a/src/cli/dm_to_user.rs b/src/cli/dm_to_user.rs index 9b8e835..9425f86 100644 --- a/src/cli/dm_to_user.rs +++ b/src/cli/dm_to_user.rs @@ -19,9 +19,12 @@ pub async fn execute_dm_to_user( None => anyhow::bail!("No trade_keys found for this order"), }; - println!("SENDING DM with trade keys: {}", trade_keys.public_key().to_hex()); + println!( + "SENDING DM with trade keys: {}", + trade_keys.public_key().to_hex() + ); send_gift_wrap_dm(client, &trade_keys, &receiver, message).await?; Ok(()) -} \ No newline at end of file +} diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index 0cf10dd..ff30dd8 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -1,11 +1,10 @@ use anyhow::Result; -use chrono::DateTime; use mostro_core::prelude::*; use nostr_sdk::prelude::*; use crate::{ - db::{connect, Order, User}, - parser::dms::parse_dm_events, + db::{connect, User}, + parser::dms::{parse_dm_events, print_direct_messages}, util::{create_filter, ListKind}, }; @@ -14,11 +13,9 @@ pub async fn execute_get_dm( trade_index: i64, mostro_keys: &Keys, client: &Client, - _from_user: bool, admin: bool, - _mostro_pubkey: &PublicKey, ) -> Result<()> { - let mut dm: Vec<(Message, u64)> = Vec::new(); + let mut dm: Vec<(Message, u64, PublicKey)> = Vec::new(); let pool = connect().await?; if !admin { for index in 1..=trade_index { @@ -39,79 +36,6 @@ pub async fn execute_get_dm( dm.extend(dm_temp); } - if dm.is_empty() { - println!(); - println!("No new messages"); - println!(); - } else { - for m in dm.iter() { - let message = m.0.get_inner_message_kind(); - let date = DateTime::from_timestamp(m.1 as i64, 0).unwrap(); - if message.id.is_some() { - println!( - "Mostro sent you this message for order id: {} at {}", - m.0.get_inner_message_kind().id.unwrap(), - date - ); - } - if let Some(payload) = &message.payload { - match payload { - Payload::PaymentRequest(_, inv, _) => { - println!(); - println!("Pay this invoice to continue --> {}", inv); - println!(); - } - Payload::TextMessage(text) => { - println!(); - println!("{text}"); - println!(); - } - Payload::Dispute(id, info) => { - println!("Action: {}", message.action); - println!("Dispute id: {}", id); - if let Some(info) = info { - println!(); - println!("Dispute info: {:#?}", info); - println!(); - } - } - Payload::CantDo(Some(cant_do_reason)) => { - println!(); - println!("Error: {:?}", cant_do_reason); - println!(); - } - Payload::Order(new_order) if message.action == Action::NewOrder => { - if new_order.id.is_some() { - let db_order = - Order::get_by_id(&pool, &new_order.id.unwrap().to_string()).await; - if db_order.is_err() { - let trade_index = message.trade_index.unwrap(); - let trade_keys = User::get_trade_keys(&pool, trade_index).await?; - let _ = Order::new(&pool, new_order.clone(), &trade_keys, None) - .await - .map_err(|e| { - anyhow::anyhow!("Failed to create DB order: {:?}", e) - })?; - } - } - println!(); - println!("Order: {:#?}", new_order); - println!(); - } - _ => { - println!(); - println!("Action: {}", message.action); - println!("Payload: {:#?}", message.payload); - println!(); - } - } - } else { - println!(); - println!("Action: {}", message.action); - println!("Payload: {:#?}", message.payload); - println!(); - } - } - } + print_direct_messages(&dm, &pool).await?; Ok(()) } diff --git a/src/cli/get_dm_user.rs b/src/cli/get_dm_user.rs index d5720a0..2d58a84 100644 --- a/src/cli/get_dm_user.rs +++ b/src/cli/get_dm_user.rs @@ -5,16 +5,16 @@ use comfy_table::presets::UTF8_FULL; use comfy_table::Table; use mostro_core::prelude::*; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; pub async fn execute_get_dm_user( since: &i64, client: &Client, mostro_pubkey: &PublicKey, + pool: &SqlitePool, ) -> Result<()> { - let pool = crate::db::connect().await?; - // Get all trade keys from orders - let mut trade_keys_hex = Order::get_all_trade_keys(&pool).await?; + let mut trade_keys_hex = Order::get_all_trade_keys(pool).await?; // Add admin private key to search for messages sent TO admin if let Ok(admin_privkey_hex) = std::env::var("NSEC_PRIVKEY") { diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index e708d42..c01146a 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -161,7 +161,7 @@ pub async fn execute_new_order( }); // Wait for the DM to be sent from mostro - wait_for_dm(client, trade_keys, request_id, trade_index, None).await?; + wait_for_dm(client, trade_keys, request_id, Some(trade_index), None).await?; Ok(()) } diff --git a/src/cli/send_msg.rs b/src/cli/send_msg.rs index 8eaf05f..b80c7b7 100644 --- a/src/cli/send_msg.rs +++ b/src/cli/send_msg.rs @@ -111,7 +111,7 @@ pub async fn execute_send_msg( }); // Wait for the DM to be sent from mostro - wait_for_dm(client, &trade_keys, request_id, 0, Some(order)).await?; + wait_for_dm(client, &trade_keys, request_id, None, Some(order)).await?; } } diff --git a/src/cli/take_buy.rs b/src/cli/take_buy.rs index 883003c..cb3c3c0 100644 --- a/src/cli/take_buy.rs +++ b/src/cli/take_buy.rs @@ -70,7 +70,7 @@ pub async fn execute_take_buy( }); // Wait for the DM to be sent from mostro - wait_for_dm(client, trade_keys, request_id, trade_index, None).await?; + wait_for_dm(client, trade_keys, request_id, Some(trade_index), None).await?; Ok(()) } diff --git a/src/cli/take_sell.rs b/src/cli/take_sell.rs index 52b9bc4..6a2e5ab 100644 --- a/src/cli/take_sell.rs +++ b/src/cli/take_sell.rs @@ -112,7 +112,7 @@ pub async fn execute_take_sell( client.subscribe(subscription, None).await?; // Wait for the DM to be sent from mostro - wait_for_dm(client, trade_keys, request_id, trade_index, None).await?; + wait_for_dm(client, trade_keys, request_id, Some(trade_index), None).await?; Ok(()) } diff --git a/src/parser/dms.rs b/src/parser/dms.rs index a896f32..110555d 100644 --- a/src/parser/dms.rs +++ b/src/parser/dms.rs @@ -1,77 +1,165 @@ +use std::collections::HashSet; + +use anyhow::Result; use base64::engine::general_purpose; use base64::Engine; +use chrono::DateTime; use mostro_core::prelude::*; use nip44::v2::{decrypt_to_bytes, ConversationKey}; use nostr_sdk::prelude::*; -pub async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message, u64)> { - let mut id_list = Vec::::new(); - let mut direct_messages: Vec<(Message, u64)> = Vec::new(); +use crate::db::{Order, User}; +use sqlx::SqlitePool; + +pub async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message, u64, PublicKey)> { + let mut id_set = HashSet::::new(); + let mut direct_messages: Vec<(Message, u64, PublicKey)> = Vec::new(); for dm in events.iter() { - if !id_list.contains(&dm.id) { - id_list.push(dm.id); + // Skip if already processed + if !id_set.insert(dm.id) { + continue; + } - let (created_at, message) = match dm.kind { - nostr_sdk::Kind::GiftWrap => { - let unwrapped_gift = match nip59::extract_rumor(pubkey, dm).await { - Ok(u) => u, - Err(_) => { - println!("Error unwrapping gift"); - continue; - } - }; - let (message, _): (Message, Option) = - serde_json::from_str(&unwrapped_gift.rumor.content).unwrap(); - (unwrapped_gift.rumor.created_at, message) - } - nostr_sdk::Kind::PrivateDirectMessage => { - let ck = - if let Ok(ck) = ConversationKey::derive(pubkey.secret_key(), &dm.pubkey) { - ck - } else { - continue; - }; - let b64decoded_content = - match general_purpose::STANDARD.decode(dm.content.as_bytes()) { - Ok(b64decoded_content) => b64decoded_content, - Err(_) => { - continue; - } - }; - let unencrypted_content = match decrypt_to_bytes(&ck, &b64decoded_content) { - Ok(bytes) => bytes, + let (created_at, message) = match dm.kind { + nostr_sdk::Kind::GiftWrap => { + let unwrapped_gift = match nip59::extract_rumor(pubkey, dm).await { + Ok(u) => u, + Err(_) => { + println!("Error unwrapping gift"); + continue; + } + }; + let (message, _): (Message, Option) = + serde_json::from_str(&unwrapped_gift.rumor.content).unwrap(); + (unwrapped_gift.rumor.created_at, message) + } + nostr_sdk::Kind::PrivateDirectMessage => { + let ck = if let Ok(ck) = ConversationKey::derive(pubkey.secret_key(), &dm.pubkey) { + ck + } else { + continue; + }; + let b64decoded_content = + match general_purpose::STANDARD.decode(dm.content.as_bytes()) { + Ok(b64decoded_content) => b64decoded_content, Err(_) => { continue; } }; - let message_str = match String::from_utf8(unencrypted_content) { - Ok(s) => s, - Err(_) => { - continue; + let unencrypted_content = match decrypt_to_bytes(&ck, &b64decoded_content) { + Ok(bytes) => bytes, + Err(_) => { + continue; + } + }; + let message_str = match String::from_utf8(unencrypted_content) { + Ok(s) => s, + Err(_) => { + continue; + } + }; + let message = match Message::from_json(&message_str) { + Ok(m) => m, + Err(_) => { + continue; + } + }; + (dm.created_at, message) + } + _ => continue, + }; + + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(30)) + .unwrap() + .timestamp() as u64; + if created_at.as_u64() < since_time { + continue; + } + direct_messages.push((message, created_at.as_u64(), pubkey.public_key())); + } + direct_messages.sort_by(|a, b| a.1.cmp(&b.1)); + direct_messages +} + +pub async fn print_direct_messages( + dm: &[(Message, u64, PublicKey)], + pool: &SqlitePool, +) -> Result<()> { + if dm.is_empty() { + println!(); + println!("No new messages"); + println!(); + } else { + for m in dm.iter() { + let message = m.0.get_inner_message_kind(); + let date = DateTime::from_timestamp(m.1 as i64, 0).unwrap(); + if message.id.is_some() { + println!( + "Mostro sent you this message for order id: {} at {}", + m.0.get_inner_message_kind().id.unwrap(), + date + ); + } + if let Some(payload) = &message.payload { + match payload { + Payload::PaymentRequest(_, inv, _) => { + println!(); + println!("Pay this invoice to continue --> {}", inv); + println!(); + } + Payload::TextMessage(text) => { + println!(); + println!("{text}"); + println!(); + } + Payload::Dispute(id, info) => { + println!("Action: {}", message.action); + println!("Dispute id: {}", id); + if let Some(info) = info { + println!(); + println!("Dispute info: {:#?}", info); + println!(); } - }; - let message = match Message::from_json(&message_str) { - Ok(m) => m, - Err(_) => { - continue; + } + Payload::CantDo(Some(cant_do_reason)) => { + println!(); + println!("Error: {:?}", cant_do_reason); + println!(); + } + Payload::Order(new_order) if message.action == Action::NewOrder => { + if new_order.id.is_some() { + let db_order = + Order::get_by_id(pool, &new_order.id.unwrap().to_string()).await; + if db_order.is_err() { + let trade_index = message.trade_index.unwrap(); + let trade_keys = User::get_trade_keys(pool, trade_index).await?; + let _ = Order::new(pool, new_order.clone(), &trade_keys, None) + .await + .map_err(|e| { + anyhow::anyhow!("Failed to create DB order: {:?}", e) + })?; + } } - }; - (dm.created_at, message) + println!(); + println!("Order: {:#?}", new_order); + println!(); + } + _ => { + println!(); + println!("Action: {}", message.action); + println!("Payload: {:#?}", message.payload); + println!(); + } } - _ => continue, - }; - - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(30)) - .unwrap() - .timestamp() as u64; - if created_at.as_u64() < since_time { - continue; + } else { + println!(); + println!("Action: {}", message.action); + println!("Payload: {:#?}", message.payload); + println!(); } - direct_messages.push((message, created_at.as_u64())); } } - direct_messages.sort_by(|a, b| a.1.cmp(&b.1)); - direct_messages + Ok(()) } diff --git a/src/util.rs b/src/util.rs index 2286773..a2bd9da 100644 --- a/src/util.rs +++ b/src/util.rs @@ -121,10 +121,16 @@ pub async fn wait_for_dm( client: &Client, trade_keys: &Keys, request_id: u64, - trade_index: i64, + trade_index: Option, mut order: Option, ) -> anyhow::Result<()> { let mut notifications = client.notifications(); + // Get trade index + let trade_index = if let Some(trade_index) = trade_index { + trade_index + } else { + return Err(anyhow::anyhow!("Trade index is required")); + }; match tokio::time::timeout(Duration::from_secs(10), async move { while let Ok(notification) = notifications.recv().await { @@ -564,7 +570,7 @@ pub async fn fetch_events_list( let direct_messages_mostro = parse_dm_events(fetched_events, mostro_keys).await; Ok(direct_messages_mostro .into_iter() - .map(|t| Event::MessageTuple(Box::new(t))) + .map(|(message, timestamp, _)| Event::MessageTuple(Box::new((message, timestamp)))) .collect()) } ListKind::DirectMessagesUser => { @@ -576,7 +582,11 @@ pub async fn fetch_events_list( client.fetch_events(filter, Duration::from_secs(15)).await?; let direct_messages_for_trade_key = parse_dm_events(fetched_user_messages, &trade_key).await; - direct_messages.extend(direct_messages_for_trade_key); + direct_messages.extend( + direct_messages_for_trade_key + .into_iter() + .map(|(message, timestamp, _)| (message, timestamp)), + ); } Ok(direct_messages .into_iter() From 0f40e8ba010e3ebc38aea52703967f9e5ef8f86c Mon Sep 17 00:00:00 2001 From: arkanoider Date: Tue, 9 Sep 2025 23:38:56 +0200 Subject: [PATCH 05/19] chore: added rabbit suggestion --- src/cli/get_dm_user.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/cli/get_dm_user.rs b/src/cli/get_dm_user.rs index 2d58a84..14dd01e 100644 --- a/src/cli/get_dm_user.rs +++ b/src/cli/get_dm_user.rs @@ -16,13 +16,17 @@ pub async fn execute_get_dm_user( // Get all trade keys from orders let mut trade_keys_hex = Order::get_all_trade_keys(pool).await?; - // Add admin private key to search for messages sent TO admin - if let Ok(admin_privkey_hex) = std::env::var("NSEC_PRIVKEY") { - trade_keys_hex.push(admin_privkey_hex); + // Include admin pubkey so we also fetch messages sent TO admin + let admin_pubkey_hex = mostro_pubkey.to_hex(); + if !trade_keys_hex.iter().any(|k| k == &admin_pubkey_hex) { + trade_keys_hex.push(admin_pubkey_hex); } + // De-duplicate any repeated keys coming from DB/admin + trade_keys_hex.sort(); + trade_keys_hex.dedup(); if trade_keys_hex.is_empty() { - println!("No trade keys found in orders and NSEC_PRIVKEY not set"); + println!("No trade keys found in orders"); return Ok(()); } From 4eb620dca5f9f731f70c4c48b72f300cce74455a Mon Sep 17 00:00:00 2001 From: arkanoider Date: Tue, 9 Sep 2025 23:39:20 +0200 Subject: [PATCH 06/19] chore: added rabbit suggestion --- src/cli/get_dm_user.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/get_dm_user.rs b/src/cli/get_dm_user.rs index 14dd01e..6c90936 100644 --- a/src/cli/get_dm_user.rs +++ b/src/cli/get_dm_user.rs @@ -16,9 +16,9 @@ pub async fn execute_get_dm_user( // Get all trade keys from orders let mut trade_keys_hex = Order::get_all_trade_keys(pool).await?; - // Include admin pubkey so we also fetch messages sent TO admin + // Include admin pubkey so we also fetch messages sent TO admin let admin_pubkey_hex = mostro_pubkey.to_hex(); - if !trade_keys_hex.iter().any(|k| k == &admin_pubkey_hex) { + if !trade_keys_hex.iter().any(|k| k == &admin_pubkey_hex) { trade_keys_hex.push(admin_pubkey_hex); } // De-duplicate any repeated keys coming from DB/admin From 0a13ad87d17c7e75b68553fbe37dd0915e821f75 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Wed, 10 Sep 2025 23:57:08 +0200 Subject: [PATCH 07/19] feat: testing flows - neworder, take sell and takebuy - fixing a db saving issue --- src/cli.rs | 26 +++++++++++++++----------- src/cli/add_invoice.rs | 1 + src/util.rs | 26 ++++++++++++++++++++------ 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index bc59a1b..397b4d7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -46,8 +46,6 @@ use take_dispute::*; use uuid::Uuid; pub static IDENTITY_KEYS: OnceLock = OnceLock::new(); -pub static MOSTRO_KEYS: OnceLock = OnceLock::new(); -pub static MOSTRO_PUBKEY: OnceLock = OnceLock::new(); pub static POOL: OnceLock = OnceLock::new(); pub static TRADE_KEY: OnceLock<(Keys, i64)> = OnceLock::new(); @@ -393,13 +391,19 @@ async fn init_context(cli: &Cli) -> Result { .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?; TRADE_KEY.get_or_init(|| (trade_keys.clone(), trade_index)); - // Get Mostro admin keys - let mostro_keys = Keys::from_str( - &std::env::var("NSEC_PRIVKEY") - .map_err(|e| anyhow::anyhow!("Failed to get mostro keys: {}", e))?, - )?; - MOSTRO_KEYS.get_or_init(|| mostro_keys.clone()); - MOSTRO_PUBKEY.get_or_init(|| mostro_keys.public_key()); + // Load Mostro admin keys if available (optional) + let mostro_keys = if let Ok(k) = std::env::var("NSEC_PRIVKEY"){ + Keys::from_str(&k)? + } else { + println!("No Mostro admin keys found"); + Keys::generate() + }; + + // Resolve Mostro pubkey from env (required for all flows) + let mostro_pubkey = PublicKey::from_str( + &std::env::var("MOSTRO_PUBKEY") + .map_err(|e| anyhow::anyhow!("Failed to get MOSTRO_PUBKEY: {}", e))?, + )?; // Connect to Nostr relays let client = util::connect_nostr().await?; @@ -410,8 +414,8 @@ async fn init_context(cli: &Cli) -> Result { trade_keys, trade_index, pool, - mostro_keys: mostro_keys.clone(), - mostro_pubkey: mostro_keys.public_key(), + mostro_keys: mostro_keys, + mostro_pubkey: mostro_pubkey, }) } diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index ae1ddae..9770945 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -16,6 +16,7 @@ pub async fn execute_add_invoice( client: &Client, pool: &SqlitePool, ) -> Result<()> { + println!("Adding invoice to order {}", order_id); let order = Order::get_by_id(pool, &order_id.to_string()).await?; let trade_keys = order .trade_keys diff --git a/src/util.rs b/src/util.rs index a2bd9da..1ab2a7b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -169,6 +169,8 @@ pub async fn wait_for_dm( "Please add a lightning invoice with amount of {}", order.amount ); + // Save the order + save_order(order.clone(), trade_keys, request_id, trade_index).await.map_err(|_| ())?; return Ok(()); } } @@ -187,6 +189,7 @@ pub async fn wait_for_dm( println!(); if let Some(order) = order { let store_order = order.clone(); + // Save the order save_order(store_order, trade_keys, request_id, trade_index).await.map_err(|_| ())?; } return Ok(()); @@ -456,17 +459,24 @@ pub async fn get_direct_messages_from_trade_keys( .unwrap() .timestamp() as u64; + // Get the triple of message, timestamp and public key let mut all_messages: Vec<(Message, u64, PublicKey)> = Vec::new(); + // Fetch direct messages from trade keys and in case of since, we filter by since + // as bonus we also fetch the events from the admin pubkey in case is specified for trade_key_hex in trade_keys_hex { if let Ok(public_key) = PublicKey::from_hex(&trade_key_hex) { + // Create filter for fetching direct messages let filter = create_filter(ListKind::DirectMessagesUser, public_key); let events = client.fetch_events(filter, Duration::from_secs(15)).await?; // Parse events without keys since we only have the public key // We'll need to handle this differently - let's just collect the events for now for event in events { if let Ok(message) = Message::from_json(&event.content) { - all_messages.push((message, event.created_at.as_u64(), public_key)); + if event.created_at.as_u64() < since as u64 { + continue; + } + all_messages.push((message, event.created_at.as_u64(), event.pubkey)); } } } @@ -528,15 +538,19 @@ pub fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { .since(fake_timestamp) } ListKind::DirectMessagesUser => { - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(30)) + // Get the timestamp from the since parameter + let fake_since = 2880; + let fake_since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(fake_since)) .unwrap() .timestamp() as u64; - let timestamp = Timestamp::from(since_time); + let fake_timestamp = Timestamp::from(fake_since_time); + + // Create filter for fetching direct messages Filter::new() - .kind(nostr_sdk::Kind::PrivateDirectMessage) + .kind(nostr_sdk::Kind::GiftWrap) .pubkey(pubkey) - .since(timestamp) + .since(fake_timestamp) } } } From d0b2990f5ef7ebdfab2a2c6441647a88096c9857 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Thu, 11 Sep 2025 23:07:49 +0200 Subject: [PATCH 08/19] refactor: collapsed in one file take order actions and tested use case of take and cancel --- src/cli.rs | 56 ++++++++++----- src/cli/add_invoice.rs | 2 +- src/cli/get_dm.rs | 9 +-- src/cli/new_order.rs | 20 ++++-- src/cli/send_msg.rs | 15 ++-- src/cli/take_buy.rs | 76 -------------------- src/cli/take_order.rs | 154 +++++++++++++++++++++++++++++++++++++++++ src/cli/take_sell.rs | 118 ------------------------------- src/util.rs | 12 ++-- 9 files changed, 228 insertions(+), 234 deletions(-) delete mode 100644 src/cli/take_buy.rs create mode 100644 src/cli/take_order.rs delete mode 100644 src/cli/take_sell.rs diff --git a/src/cli.rs b/src/cli.rs index 397b4d7..b31f2e7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,9 +11,8 @@ pub mod rate_user; pub mod restore; pub mod send_dm; pub mod send_msg; -pub mod take_buy; pub mod take_dispute; -pub mod take_sell; +pub mod take_order; use crate::cli::add_invoice::execute_add_invoice; use crate::cli::adm_send_dm::execute_adm_send_dm; @@ -27,14 +26,14 @@ use crate::cli::new_order::execute_new_order; use crate::cli::rate_user::execute_rate_user; use crate::cli::restore::execute_restore; use crate::cli::send_dm::execute_send_dm; -use crate::cli::take_buy::execute_take_buy; use crate::cli::take_dispute::execute_take_dispute; -use crate::cli::take_sell::execute_take_sell; +use crate::cli::take_order::execute_take_order; use crate::db::{connect, User}; use crate::util; use anyhow::{Error, Result}; use clap::{Parser, Subcommand}; +use mostro_core::prelude::*; use nostr_sdk::prelude::*; use sqlx::SqlitePool; use std::sync::OnceLock; @@ -391,19 +390,19 @@ async fn init_context(cli: &Cli) -> Result { .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?; TRADE_KEY.get_or_init(|| (trade_keys.clone(), trade_index)); - // Load Mostro admin keys if available (optional) - let mostro_keys = if let Ok(k) = std::env::var("NSEC_PRIVKEY"){ + // Load Mostro admin keys if available (optional) + let mostro_keys = if let Ok(k) = std::env::var("NSEC_PRIVKEY") { Keys::from_str(&k)? } else { println!("No Mostro admin keys found"); Keys::generate() }; - // Resolve Mostro pubkey from env (required for all flows) - let mostro_pubkey = PublicKey::from_str( - &std::env::var("MOSTRO_PUBKEY") - .map_err(|e| anyhow::anyhow!("Failed to get MOSTRO_PUBKEY: {}", e))?, - )?; + // Resolve Mostro pubkey from env (required for all flows) + let mostro_pubkey = PublicKey::from_str( + &std::env::var("MOSTRO_PUBKEY") + .map_err(|e| anyhow::anyhow!("Failed to get MOSTRO_PUBKEY: {}", e))?, + )?; // Connect to Nostr relays let client = util::connect_nostr().await?; @@ -414,8 +413,8 @@ async fn init_context(cli: &Cli) -> Result { trade_keys, trade_index, pool, - mostro_keys: mostro_keys, - mostro_pubkey: mostro_pubkey, + mostro_keys, + mostro_pubkey, }) } @@ -433,6 +432,7 @@ impl Commands { &ctx.identity_keys, ctx.mostro_pubkey, &ctx.client, + &ctx.pool, ) .await } @@ -502,6 +502,7 @@ impl Commands { ctx.mostro_pubkey, &ctx.client, expiration_days, + &ctx.pool, ) .await } @@ -510,8 +511,9 @@ impl Commands { invoice, amount, } => { - execute_take_sell( + execute_take_order( order_id, + Action::TakeSell, invoice, *amount, &ctx.identity_keys, @@ -519,18 +521,22 @@ impl Commands { ctx.trade_index, ctx.mostro_pubkey, &ctx.client, + &ctx.pool, ) .await } Commands::TakeBuy { order_id, amount } => { - execute_take_buy( + execute_take_order( order_id, + Action::TakeBuy, + &None, *amount, &ctx.identity_keys, &ctx.trade_keys, ctx.trade_index, ctx.mostro_pubkey, &ctx.client, + &ctx.pool, ) .await } @@ -558,13 +564,29 @@ impl Commands { // DM retrieval commands Commands::GetDm { since } => { - execute_get_dm(since, ctx.trade_index, &ctx.mostro_keys, &ctx.client, false).await + execute_get_dm( + since, + ctx.trade_index, + &ctx.mostro_keys, + &ctx.client, + false, + &ctx.pool, + ) + .await } Commands::GetDmUser { since } => { execute_get_dm_user(since, &ctx.client, &ctx.mostro_pubkey, &ctx.pool).await } Commands::GetAdminDm { since } => { - execute_get_dm(since, ctx.trade_index, &ctx.mostro_keys, &ctx.client, true).await + execute_get_dm( + since, + ctx.trade_index, + &ctx.mostro_keys, + &ctx.client, + true, + &ctx.pool, + ) + .await } // Admin commands diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index 9770945..da00a69 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -71,6 +71,6 @@ pub async fn execute_add_invoice( .await?; // Wait for the DM to be sent from mostro and update the order - wait_for_dm(client, &trade_keys, request_id, None, Some(order)).await?; + wait_for_dm(client, &trade_keys, request_id, None, Some(order), pool).await?; Ok(()) } diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index ff30dd8..11a3332 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -1,9 +1,10 @@ use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; use crate::{ - db::{connect, User}, + db::User, parser::dms::{parse_dm_events, print_direct_messages}, util::{create_filter, ListKind}, }; @@ -14,12 +15,12 @@ pub async fn execute_get_dm( mostro_keys: &Keys, client: &Client, admin: bool, + pool: &SqlitePool, ) -> Result<()> { let mut dm: Vec<(Message, u64, PublicKey)> = Vec::new(); - let pool = connect().await?; if !admin { for index in 1..=trade_index { - let keys = User::get_trade_keys(&pool, index).await?; + let keys = User::get_trade_keys(pool, index).await?; let filter = create_filter(ListKind::DirectMessagesUser, keys.public_key()); let fetched_events = client .fetch_events(filter, std::time::Duration::from_secs(15)) @@ -36,6 +37,6 @@ pub async fn execute_get_dm( dm.extend(dm_temp); } - print_direct_messages(&dm, &pool).await?; + print_direct_messages(&dm, pool).await?; Ok(()) } diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index c01146a..19fc439 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -1,15 +1,15 @@ +use crate::parser::orders::print_order_preview; +use crate::util::{send_dm, uppercase_first, wait_for_dm}; use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; use std::collections::HashMap; use std::io::{stdin, stdout, BufRead, Write}; use std::process; use std::str::FromStr; use uuid::Uuid; -use crate::parser::orders::print_order_preview; -use crate::util::{send_dm, uppercase_first, wait_for_dm}; - pub type FiatNames = HashMap; #[allow(clippy::too_many_arguments)] @@ -27,6 +27,7 @@ pub async fn execute_new_order( mostro_key: PublicKey, client: &Client, expiration_days: &i64, + pool: &SqlitePool, ) -> Result<()> { // Uppercase currency let fiat_code = fiat_code.to_uppercase(); @@ -47,7 +48,8 @@ pub async fn execute_new_order( } let kind = uppercase_first(kind); // New check against strings - let kind_checked = mostro_core::order::Kind::from_str(&kind).unwrap(); + let kind_checked = mostro_core::order::Kind::from_str(&kind) + .map_err(|_| anyhow::anyhow!("Invalid order kind"))?; let expires_at = match *expiration_days { 0 => None, _ => { @@ -161,7 +163,15 @@ pub async fn execute_new_order( }); // Wait for the DM to be sent from mostro - wait_for_dm(client, trade_keys, request_id, Some(trade_index), None).await?; + wait_for_dm( + client, + trade_keys, + request_id, + Some(trade_index), + None, + pool, + ) + .await?; Ok(()) } diff --git a/src/cli/send_msg.rs b/src/cli/send_msg.rs index b80c7b7..5f518bd 100644 --- a/src/cli/send_msg.rs +++ b/src/cli/send_msg.rs @@ -1,6 +1,6 @@ +use crate::cli::Commands; use crate::db::{Order, User}; use crate::util::wait_for_dm; -use crate::{cli::Commands, db::connect}; use anyhow::Result; use mostro_core::prelude::*; @@ -16,6 +16,7 @@ pub async fn execute_send_msg( mostro_key: PublicKey, client: &Client, text: Option<&str>, + pool: &SqlitePool, ) -> Result<()> { // Map CLI command to action let requested_action = match command { @@ -39,20 +40,18 @@ pub async fn execute_send_msg( mostro_key ); - let pool = connect().await?; - // Determine payload let payload = match requested_action { - Action::FiatSent | Action::Release => create_next_trade_payload(&pool, &order_id).await?, + Action::FiatSent | Action::Release => create_next_trade_payload(pool, &order_id).await?, _ => text.map(|t| Payload::TextMessage(t.to_string())), }; // Update last trade index if next trade payload if let Some(Payload::NextTrade(_, trade_index)) = &payload { // Update last trade index - match User::get(&pool).await { + match User::get(pool).await { Ok(mut user) => { user.set_last_trade_index(*trade_index as i64); - if let Err(e) = user.save(&pool).await { + if let Err(e) = user.save(pool).await { println!("Failed to update user: {}", e); } } @@ -70,7 +69,7 @@ pub async fn execute_send_msg( .to_owned(); if let Some(order_id) = order_id { - let order = Order::get_by_id(&pool, &order_id.to_string()).await?; + let order = Order::get_by_id(pool, &order_id.to_string()).await?; if let Some(trade_keys_str) = order.trade_keys.clone() { let trade_keys = Keys::parse(&trade_keys_str)?; @@ -111,7 +110,7 @@ pub async fn execute_send_msg( }); // Wait for the DM to be sent from mostro - wait_for_dm(client, &trade_keys, request_id, None, Some(order)).await?; + wait_for_dm(client, &trade_keys, request_id, None, Some(order), pool).await?; } } diff --git a/src/cli/take_buy.rs b/src/cli/take_buy.rs deleted file mode 100644 index cb3c3c0..0000000 --- a/src/cli/take_buy.rs +++ /dev/null @@ -1,76 +0,0 @@ -use anyhow::Result; -use mostro_core::prelude::*; -use nostr_sdk::prelude::*; -use uuid::Uuid; - -use crate::util::{send_dm, wait_for_dm}; - -pub async fn execute_take_buy( - order_id: &Uuid, - amount: Option, - identity_keys: &Keys, - trade_keys: &Keys, - trade_index: i64, - mostro_key: PublicKey, - client: &Client, -) -> Result<()> { - println!( - "Request of take buy order {} from mostro pubId {}", - order_id, - mostro_key.clone() - ); - let request_id = Uuid::new_v4().as_u128() as u64; - let payload = amount.map(|amt: u32| Payload::Amount(amt as i64)); - // Create takebuy message - let take_buy_message = Message::new_order( - Some(*order_id), - Some(request_id), - Some(trade_index), - Action::TakeBuy, - payload, - ); - - // Send dm to receiver pubkey - println!( - "SENDING DM with trade keys: {:?}", - trade_keys.public_key().to_hex() - ); - - let message_json = take_buy_message - .as_json() - .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - - // Clone the keys and client for the async call - let identity_keys = identity_keys.clone(); - let trade_keys_clone = trade_keys.clone(); - let client_clone = client.clone(); - // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 - let subscription = Filter::new() - .pubkey(trade_keys.public_key()) - .kind(nostr_sdk::Kind::GiftWrap) - .limit(0); - // Subscribe to gift wrap events -waiting for 1 event - let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); - //Activate the subscription - client.subscribe(subscription, Some(opts)).await?; - - // Spawn a new task to send the DM - // This is so we can wait for the gift wrap event in the main thread - tokio::spawn(async move { - let _ = send_dm( - &client_clone, - Some(&identity_keys.clone()), - &trade_keys_clone, - &mostro_key, - message_json, - None, - false, - ) - .await; - }); - - // Wait for the DM to be sent from mostro - wait_for_dm(client, trade_keys, request_id, Some(trade_index), None).await?; - - Ok(()) -} diff --git a/src/cli/take_order.rs b/src/cli/take_order.rs new file mode 100644 index 0000000..482e01b --- /dev/null +++ b/src/cli/take_order.rs @@ -0,0 +1,154 @@ +use anyhow::Result; +use lnurl::lightning_address::LightningAddress; +use mostro_core::prelude::*; +use nostr_sdk::prelude::*; +use sqlx::SqlitePool; +use std::str::FromStr; +use uuid::Uuid; + +use crate::lightning::is_valid_invoice; +use crate::util; + +/// Create payload based on action type and parameters +fn create_take_order_payload( + action: Action, + invoice: &Option, + amount: Option, +) -> Result> { + match action { + Action::TakeBuy => Ok(amount.map(|amt: u32| Payload::Amount(amt as i64))), + Action::TakeSell => Ok(Some(match invoice { + Some(inv) => { + let initial_payload = match LightningAddress::from_str(inv) { + Ok(_) => Payload::PaymentRequest(None, inv.to_string(), None), + Err(_) => match is_valid_invoice(inv) { + Ok(i) => Payload::PaymentRequest(None, i.to_string(), None), + Err(e) => { + println!("{}", e); + Payload::PaymentRequest(None, inv.to_string(), None) + } + }, + }; + + match amount { + Some(amt) => match initial_payload { + Payload::PaymentRequest(a, b, _) => { + Payload::PaymentRequest(a, b, Some(amt as i64)) + } + payload => payload, + }, + None => initial_payload, + } + } + None => amount + .map(|amt| Payload::Amount(amt.into())) + .unwrap_or(Payload::Amount(0)), + })), + _ => Err(anyhow::anyhow!("Invalid action for take order")), + } +} + +/// Unified function to handle both take buy and take sell orders +#[allow(clippy::too_many_arguments)] +pub async fn execute_take_order( + order_id: &Uuid, + action: Action, + invoice: &Option, + amount: Option, + identity_keys: &Keys, + trade_keys: &Keys, + trade_index: i64, + mostro_key: PublicKey, + client: &Client, + pool: &SqlitePool, +) -> Result<()> { + let action_name = match action { + Action::TakeBuy => "take buy", + Action::TakeSell => "take sell", + _ => return Err(anyhow::anyhow!("Invalid action for take order")), + }; + + println!( + "Request of {} order {} from mostro pubId {}", + action_name, + order_id, + mostro_key.clone() + ); + + // Create payload based on action type + let payload = create_take_order_payload(action.clone(), invoice, amount)?; + + let request_id = Uuid::new_v4().as_u128() as u64; + + // Create message + let take_order_message = Message::new_order( + Some(*order_id), + Some(request_id), + Some(trade_index), + action.clone(), + payload, + ); + + // Send dm to receiver pubkey + println!( + "SENDING DM with trade keys: {:?}", + trade_keys.public_key().to_hex() + ); + + let message_json = take_order_message + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; + + // Clone the keys and client for the async call + let identity_keys = identity_keys.clone(); + let trade_keys_clone = trade_keys.clone(); + let client_clone = client.clone(); + + // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 + let subscription = Filter::new() + .pubkey(trade_keys.public_key()) + .kind(nostr_sdk::Kind::GiftWrap) + .limit(0); + + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); + client.subscribe(subscription, Some(opts)).await?; + + // Spawn a new task to send the DM + // This is so we can wait for the gift wrap event in the main thread + tokio::spawn(async move { + let _ = util::send_dm( + &client_clone, + Some(&identity_keys.clone()), + &trade_keys_clone, + &mostro_key, + message_json, + None, + false, + ) + .await; + }); + + // For take_sell, add an additional subscription with timestamp filtering + if action == Action::TakeSell { + let subscription = Filter::new() + .pubkey(trade_keys.public_key()) + .kind(nostr_sdk::Kind::GiftWrap) + .since(Timestamp::from(chrono::Utc::now().timestamp() as u64)) + .limit(0); + + client.subscribe(subscription, None).await?; + } + + // Wait for the DM to be sent from mostro + util::wait_for_dm( + client, + trade_keys, + request_id, + Some(trade_index), + None, + pool, + ) + .await?; + + Ok(()) +} diff --git a/src/cli/take_sell.rs b/src/cli/take_sell.rs deleted file mode 100644 index 6a2e5ab..0000000 --- a/src/cli/take_sell.rs +++ /dev/null @@ -1,118 +0,0 @@ -use anyhow::Result; -use lnurl::lightning_address::LightningAddress; -use mostro_core::prelude::*; - -use nostr_sdk::prelude::*; -use std::str::FromStr; -use uuid::Uuid; - -use crate::lightning::is_valid_invoice; -use crate::util::{send_dm, wait_for_dm}; - -#[allow(clippy::too_many_arguments)] -pub async fn execute_take_sell( - order_id: &Uuid, - invoice: &Option, - amount: Option, - identity_keys: &Keys, - trade_keys: &Keys, - trade_index: i64, - mostro_key: PublicKey, - client: &Client, -) -> Result<()> { - println!( - "Request of take sell order {} from mostro pubId {}", - order_id, - mostro_key.clone() - ); - - let payload = match invoice { - Some(inv) => { - let initial_payload = match LightningAddress::from_str(inv) { - Ok(_) => Payload::PaymentRequest(None, inv.to_string(), None), - Err(_) => match is_valid_invoice(inv) { - Ok(i) => Payload::PaymentRequest(None, i.to_string(), None), - Err(e) => { - println!("{}", e); - Payload::PaymentRequest(None, inv.to_string(), None) // or handle error differently - } - }, - }; - - match amount { - Some(amt) => match initial_payload { - Payload::PaymentRequest(a, b, _) => { - Payload::PaymentRequest(a, b, Some(amt as i64)) - } - payload => payload, - }, - None => initial_payload, - } - } - None => amount - .map(|amt| Payload::Amount(amt.into())) - .unwrap_or(Payload::Amount(0)), - }; - - let request_id = Uuid::new_v4().as_u128() as u64; - // Create takesell message - let take_sell_message = Message::new_order( - Some(*order_id), - Some(request_id), - Some(trade_index), - Action::TakeSell, - Some(payload), - ); - - // Send dm to receiver pubkey - println!( - "SENDING DM with trade keys: {:?}", - trade_keys.public_key().to_hex() - ); - let message_json = take_sell_message - .as_json() - .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - - // Clone the keys and client for the async call - let identity_keys = identity_keys.clone(); - let trade_keys_clone = trade_keys.clone(); - let client_clone = client.clone(); - // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 - let subscription = Filter::new() - .pubkey(trade_keys.public_key()) - .kind(nostr_sdk::Kind::GiftWrap) - .limit(0); - - let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); - - client.subscribe(subscription, Some(opts)).await?; - - // Spawn a new task to send the DM - // This is so we can wait for the gift wrap event in the main thread - tokio::spawn(async move { - let _ = send_dm( - &client_clone, - Some(&identity_keys.clone()), - &trade_keys_clone, - &mostro_key, - message_json, - None, - false, - ) - .await; - }); - - // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 - let subscription = Filter::new() - .pubkey(trade_keys.public_key()) - .kind(nostr_sdk::Kind::GiftWrap) - .since(Timestamp::from(chrono::Utc::now().timestamp() as u64)) - .limit(2); - - client.subscribe(subscription, None).await?; - - // Wait for the DM to be sent from mostro - wait_for_dm(client, trade_keys, request_id, Some(trade_index), None).await?; - - Ok(()) -} diff --git a/src/util.rs b/src/util.rs index 1ab2a7b..483a83c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -123,13 +123,15 @@ pub async fn wait_for_dm( request_id: u64, trade_index: Option, mut order: Option, + pool: &SqlitePool, ) -> anyhow::Result<()> { let mut notifications = client.notifications(); // Get trade index let trade_index = if let Some(trade_index) = trade_index { trade_index } else { - return Err(anyhow::anyhow!("Trade index is required")); + println!("Trade index not used for this action"); + 0 }; match tokio::time::timeout(Duration::from_secs(10), async move { @@ -219,14 +221,12 @@ pub async fn wait_for_dm( Action::Canceled => { if let Some(order_id) = &message.id { // Acquire database connection - let pool = connect().await.map_err(|_| ())?; // Verify order exists before deletion - if Order::get_by_id(&pool, &order_id.to_string()).await.is_ok() { - Order::delete_by_id(&pool, &order_id.to_string()) + if Order::get_by_id(pool, &order_id.to_string()).await.is_ok() { + Order::delete_by_id(pool, &order_id.to_string()) .await .map_err(|_| ())?; // Release database connection - drop(pool); println!("Order {} canceled!", order_id); return Ok(()); } else { @@ -644,6 +644,7 @@ pub async fn run_simple_order_msg( identity_keys: &Keys, mostro_key: PublicKey, client: &Client, + pool: &SqlitePool, ) -> Result<()> { execute_send_msg( command, @@ -652,6 +653,7 @@ pub async fn run_simple_order_msg( mostro_key, client, None, + pool, ) .await } From 9c06fc7dfa9067193a1390560c86b38619e4cfab Mon Sep 17 00:00:00 2001 From: arkanoider Date: Sun, 14 Sep 2025 00:04:43 +0200 Subject: [PATCH 09/19] refactor: executed rabbit advices where good, some cosmetics --- src/cli.rs | 1 + src/cli/get_dm.rs | 2 +- src/cli/rate_user.rs | 22 ++++++------- src/cli/send_msg.rs | 50 +++++++++++++----------------- src/parser/dms.rs | 2 +- src/parser/orders.rs | 32 +++++++++++-------- src/util.rs | 73 ++++++++++++++++++++++++++++++-------------- 7 files changed, 102 insertions(+), 80 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index b31f2e7..feeff3c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -558,6 +558,7 @@ impl Commands { &ctx.identity_keys, ctx.mostro_pubkey, &ctx.client, + &ctx.pool, ) .await } diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index 11a3332..6bb4e33 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -29,7 +29,7 @@ pub async fn execute_get_dm( dm.extend(dm_temp); } } else { - let filter = create_filter(ListKind::DirectMessagesAdmin, mostro_keys.public_key()); + let filter = create_filter(ListKind::DirectMessagesMostro, mostro_keys.public_key()); let fetched_events = client .fetch_events(filter, std::time::Duration::from_secs(15)) .await?; diff --git a/src/cli/rate_user.rs b/src/cli/rate_user.rs index 2a297a3..e9e8962 100644 --- a/src/cli/rate_user.rs +++ b/src/cli/rate_user.rs @@ -1,12 +1,12 @@ use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; use uuid::Uuid; -use crate::{ - db::{connect, Order}, - util::send_message_sync, -}; +const RATING_BOUNDARIES: [u8; 5] = [1, 2, 3, 4, 5]; + +use crate::{db::Order, util::send_message_sync}; pub async fn execute_rate_user( order_id: &Uuid, @@ -14,21 +14,17 @@ pub async fn execute_rate_user( identity_keys: &Keys, mostro_key: PublicKey, client: &Client, + pool: &SqlitePool, ) -> Result<()> { - // User rating - let rating_content; - // Check boundaries - if let 1..=5 = *rating { - rating_content = Payload::RatingUser(*rating); + let rating_content = if let Some(rating) = RATING_BOUNDARIES.iter().find(|r| r == &rating) { + Payload::RatingUser(*rating) } else { println!("Rating must be in the range 1 - 5"); std::process::exit(0); - } - - let pool = connect().await?; + }; - let trade_keys = if let Ok(order_to_vote) = Order::get_by_id(&pool, &order_id.to_string()).await + let trade_keys = if let Ok(order_to_vote) = Order::get_by_id(pool, &order_id.to_string()).await { match order_to_vote.trade_keys.as_ref() { Some(trade_keys) => Keys::parse(trade_keys)?, diff --git a/src/cli/send_msg.rs b/src/cli/send_msg.rs index 5f518bd..019622f 100644 --- a/src/cli/send_msg.rs +++ b/src/cli/send_msg.rs @@ -1,6 +1,6 @@ use crate::cli::Commands; use crate::db::{Order, User}; -use crate::util::wait_for_dm; +use crate::util::{send_dm, wait_for_dm}; use anyhow::Result; use mostro_core::prelude::*; @@ -59,11 +59,11 @@ pub async fn execute_send_msg( } } + // Create request id let request_id = Uuid::new_v4().as_u128() as u64; // Create and send the message let message = Message::new_order(order_id, Some(request_id), None, requested_action, payload); - let client_clone = client.clone(); let idkey = identity_keys .ok_or_else(|| anyhow::anyhow!("Identity keys are required"))? .to_owned(); @@ -81,36 +81,28 @@ pub async fn execute_send_msg( let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); - + // Subscribe to gift wrap events client.subscribe(subscription, Some(opts)).await?; - // Clone the keys and client for the async call - let trade_keys_clone = trade_keys.clone(); - - // Spawn a new task to send the DM - // This is so we can wait for the gift wrap event in the main thread - tokio::spawn(async move { - match message.as_json() { - Ok(message_json) => { - if let Err(e) = crate::util::send_dm( - &client_clone, - Some(&idkey), - &trade_keys_clone, - &mostro_key, - message_json, - None, - false, - ) - .await - { - eprintln!("Failed to send DM: {}", e); - } - } - Err(e) => eprintln!("Failed to serialize message: {}", e), - } - }); + // Send DM + let message_json = message + .as_json() + .map_err(|e| anyhow::anyhow!("Failed to serialize message: {e}"))?; + send_dm( + client, + Some(&idkey), + &trade_keys, + &mostro_key, + message_json, + None, + false, + ) + .await + .map_err(|e| anyhow::anyhow!("Failed to send DM: {e}"))?; // Wait for the DM to be sent from mostro - wait_for_dm(client, &trade_keys, request_id, None, Some(order), pool).await?; + wait_for_dm(client, &trade_keys, request_id, None, Some(order), pool) + .await + .map_err(|e| anyhow::anyhow!("Failed to wait for DM: {e}"))?; } } diff --git a/src/parser/dms.rs b/src/parser/dms.rs index 110555d..f134adb 100644 --- a/src/parser/dms.rs +++ b/src/parser/dms.rs @@ -77,7 +77,7 @@ pub async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message, u64 if created_at.as_u64() < since_time { continue; } - direct_messages.push((message, created_at.as_u64(), pubkey.public_key())); + direct_messages.push((message, created_at.as_u64(), dm.pubkey)); } direct_messages.sort_by(|a, b| a.1.cmp(&b.1)); direct_messages diff --git a/src/parser/orders.rs b/src/parser/orders.rs index ee4e9d1..1fcaef1 100644 --- a/src/parser/orders.rs +++ b/src/parser/orders.rs @@ -35,30 +35,36 @@ pub fn parse_orders_events( continue; } + // Check if order kind is none if order.kind.is_none() { info!("Order kind is none"); continue; } - if order.status.is_none() { - info!("Order status is none"); - continue; + // Check if order status is none + if let Some(filter_status) = status { + if order.status != Some(filter_status) { + continue; + } + } + // Check if order fiat code is none + if let Some(ref curr) = currency { + if order.fiat_code != *curr { + continue; + } } // Get created at field from Nostr event - order.created_at = Some(event.created_at.as_u64() as i64); - complete_events_list.push(order.clone()); - if order.status.ne(&status) { - continue; + if let Some(ref k) = kind { + if order.kind.as_ref() != Some(k) { + continue; + } } - if currency.is_some() && order.fiat_code.ne(¤cy.clone().unwrap()) { - continue; - } + // Get created at field from Nostr event + order.created_at = Some(event.created_at.as_u64() as i64); + complete_events_list.push(order.clone()); - if kind.is_some() && order.kind.ne(&kind) { - continue; - } // Add just requested orders requested by filtering requested_orders_list.push(order); } diff --git a/src/util.rs b/src/util.rs index 483a83c..71fc57b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,6 @@ use crate::cli::send_msg::execute_send_msg; use crate::cli::Commands; -use crate::db::{connect, Order, User}; +use crate::db::{Order, User}; use crate::parser::{parse_dispute_events, parse_dm_events, parse_orders_events}; use anyhow::{Error, Result}; use base64::engine::general_purpose; @@ -27,7 +27,7 @@ pub enum ListKind { Orders, Disputes, DirectMessagesUser, - DirectMessagesAdmin, + DirectMessagesMostro, } async fn send_gift_wrap_dm_internal( @@ -94,19 +94,19 @@ pub async fn save_order( trade_keys: &Keys, request_id: u64, trade_index: i64, + pool: &SqlitePool, ) -> Result<()> { - let pool = connect().await?; - if let Ok(order) = Order::new(&pool, order, trade_keys, Some(request_id as i64)).await { + if let Ok(order) = Order::new(pool, order, trade_keys, Some(request_id as i64)).await { if let Some(order_id) = order.id { println!("Order {} created", order_id); } else { println!("Warning: The newly created order has no ID."); } // Update last trade index to be used in next trade - match User::get(&pool).await { + match User::get(pool).await { Ok(mut user) => { user.set_last_trade_index(trade_index); - if let Err(e) = user.save(&pool).await { + if let Err(e) = user.save(pool).await { println!("Failed to update user: {}", e); } } @@ -138,14 +138,29 @@ pub async fn wait_for_dm( while let Ok(notification) = notifications.recv().await { if let RelayPoolNotification::Event { event, .. } = notification { if event.kind == nostr_sdk::Kind::GiftWrap { - let gift = nip59::extract_rumor(trade_keys, &event).await.unwrap(); - let (message, _): (Message, Option) = serde_json::from_str(&gift.rumor.content).unwrap(); + let gift = match nip59::extract_rumor(trade_keys, &event).await { + Ok(gift) => gift, + Err(e) => { + println!("Failed to extract rumor: {}", e); + continue; + } + }; + let (message, _): (Message, Option) = match serde_json::from_str(&gift.rumor.content) { + Ok(msg) => msg, + Err(e) => { + println!("Failed to deserialize message: {}", e); + continue; + } + }; let message = message.get_inner_message_kind(); if message.request_id == Some(request_id) { match message.action { Action::NewOrder => { if let Some(Payload::Order(order)) = message.payload.as_ref() { - save_order(order.clone(), trade_keys, request_id, trade_index).await.map_err(|_| ())?; + if let Err(e) = save_order(order.clone(), trade_keys, request_id, trade_index, pool).await { + println!("Failed to save order: {}", e); + return Err(()); + } return Ok(()); } } @@ -153,10 +168,9 @@ pub async fn wait_for_dm( Action::WaitingSellerToPay => { println!("Now we should wait for the seller to pay the invoice"); if let Some(mut order) = order.take() { - let pool = connect().await.map_err(|_| ())?; match order .set_status(Status::WaitingPayment.to_string()) - .save(&pool) + .save(pool) .await { Ok(_) => println!("Order status updated"), @@ -172,7 +186,10 @@ pub async fn wait_for_dm( order.amount ); // Save the order - save_order(order.clone(), trade_keys, request_id, trade_index).await.map_err(|_| ())?; + if let Err(e) = save_order(order.clone(), trade_keys, request_id, trade_index, pool).await { + println!("Failed to save order: {}", e); + return Err(()); + } return Ok(()); } } @@ -192,7 +209,10 @@ pub async fn wait_for_dm( if let Some(order) = order { let store_order = order.clone(); // Save the order - save_order(store_order, trade_keys, request_id, trade_index).await.map_err(|_| ())?; + if let Err(e) = save_order(store_order, trade_keys, request_id, trade_index, pool).await { + println!("Failed to save order: {}", e); + return Err(()); + } } return Ok(()); } @@ -223,9 +243,10 @@ pub async fn wait_for_dm( // Acquire database connection // Verify order exists before deletion if Order::get_by_id(pool, &order_id.to_string()).await.is_ok() { - Order::delete_by_id(pool, &order_id.to_string()) - .await - .map_err(|_| ())?; + if let Err(e) = Order::delete_by_id(pool, &order_id.to_string()).await { + println!("Failed to delete order: {}", e); + return Err(()); + } // Release database connection println!("Order {} canceled!", order_id); return Ok(()); @@ -247,7 +268,10 @@ pub async fn wait_for_dm( Ok(()) }) .await { - Ok(_) => Ok(()), + Ok(result) => match result { + Ok(()) => Ok(()), + Err(()) => Err(anyhow::anyhow!("Error in timeout closure")), + }, Err(_) => Err(anyhow::anyhow!("Timeout waiting for DM or gift wrap event")) } } @@ -307,18 +331,21 @@ async fn create_gift_wrap_event( expiration: Option, signed: bool, ) -> Result { - let message = Message::from_json(&payload).unwrap(); + let message = Message::from_json(&payload) + .map_err(|e| anyhow::anyhow!("Failed to deserialize message: {e}"))?; let content = if signed { let _identity_keys = identity_keys .ok_or_else(|| Error::msg("identity_keys required for signed messages"))?; // We sign the message let sig = Message::sign(payload, trade_keys); - serde_json::to_string(&(message, sig)).unwrap() + serde_json::to_string(&(message, sig)) + .map_err(|e| anyhow::anyhow!("Failed to serialize message: {e}"))? } else { // We compose the content, when private we don't sign the payload let content: (Message, Option) = (message, None); - serde_json::to_string(&content).unwrap() + serde_json::to_string(&content) + .map_err(|e| anyhow::anyhow!("Failed to serialize message: {e}"))? }; // We create the rumor @@ -329,7 +356,7 @@ async fn create_gift_wrap_event( let tags = create_expiration_tags(expiration); let signer_keys = if signed { - identity_keys.unwrap() + identity_keys.ok_or_else(|| Error::msg("identity_keys required for signed messages"))? } else { trade_keys }; @@ -522,7 +549,7 @@ pub fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { ) .kind(nostr_sdk::Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND)) } - ListKind::DirectMessagesAdmin => { + ListKind::DirectMessagesMostro => { // We use a fake timestamp to thwart time-analysis attacks let fake_since = 2880; let fake_since_time = chrono::Utc::now() @@ -576,7 +603,7 @@ pub async fn fetch_events_list( let orders = parse_orders_events(fetched_events, currency, status, kind); Ok(orders.into_iter().map(Event::SmallOrder).collect()) } - ListKind::DirectMessagesAdmin => { + ListKind::DirectMessagesMostro => { let filters = create_filter(list_kind, mostro_keys.public_key()); let fetched_events = client .fetch_events(filters, Duration::from_secs(15)) From 7b77c234d59e105ffa96a37283fda58073e28c96 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Sun, 14 Sep 2025 00:13:13 +0200 Subject: [PATCH 10/19] refactor: unwrap removal from list_order.rs and dms.rs --- src/cli/list_orders.rs | 31 ++++++++++++++--------- src/parser/dms.rs | 56 ++++++++++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/cli/list_orders.rs b/src/cli/list_orders.rs index ac2cccd..44bf24d 100644 --- a/src/cli/list_orders.rs +++ b/src/cli/list_orders.rs @@ -19,33 +19,40 @@ pub async fn execute_list_orders( ) -> Result<()> { // Used to get upper currency string to check against a list of tickers let mut upper_currency: Option = None; - let mut status_checked: Option = Some(Status::from_str("pending").unwrap()); + let mut status_checked: Option = Some( + Status::from_str("pending") + .map_err(|e| anyhow::anyhow!("Invalid default status 'pending': {:?}", e))?, + ); let mut kind_checked: Option = None; // New check against strings if let Some(s) = status { - status_checked = Some(Status::from_str(s).expect("Not valid status! Please check")); + status_checked = Some( + Status::from_str(s) + .map_err(|e| anyhow::anyhow!("Not valid status '{}': {:?}", s, e))?, + ); } - println!( - "You are searching orders with status {:?}", - status_checked.unwrap() - ); + if let Some(status) = &status_checked { + println!("You are searching orders with status {:?}", status); + } // New check against strings if let Some(k) = kind { kind_checked = Some( - mostro_core::order::Kind::from_str(k).expect("Not valid order kind! Please check"), + mostro_core::order::Kind::from_str(k) + .map_err(|e| anyhow::anyhow!("Not valid order kind '{}': {:?}", k, e))?, ); - println!("You are searching {} orders", kind_checked.unwrap()); + if let Some(kind) = &kind_checked { + println!("You are searching {} orders", kind); + } } // Uppercase currency if let Some(curr) = currency { upper_currency = Some(curr.to_uppercase()); - println!( - "You are searching orders with currency {}", - upper_currency.clone().unwrap() - ); + if let Some(currency) = &upper_currency { + println!("You are searching orders with currency {}", currency); + } } println!("Requesting orders from mostro pubId - {}", mostro_pubkey); diff --git a/src/parser/dms.rs b/src/parser/dms.rs index f134adb..6ae2b44 100644 --- a/src/parser/dms.rs +++ b/src/parser/dms.rs @@ -31,7 +31,13 @@ pub async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message, u64 } }; let (message, _): (Message, Option) = - serde_json::from_str(&unwrapped_gift.rumor.content).unwrap(); + match serde_json::from_str(&unwrapped_gift.rumor.content) { + Ok(msg) => msg, + Err(_) => { + println!("Error parsing gift wrap content"); + continue; + } + }; (unwrapped_gift.rumor.created_at, message) } nostr_sdk::Kind::PrivateDirectMessage => { @@ -70,10 +76,14 @@ pub async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message, u64 _ => continue, }; - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(30)) - .unwrap() - .timestamp() as u64; + let since_time = match chrono::Utc::now().checked_sub_signed(chrono::Duration::minutes(30)) + { + Some(dt) => dt.timestamp() as u64, + None => { + println!("Error: Unable to calculate time 30 minutes ago"); + continue; + } + }; if created_at.as_u64() < since_time { continue; } @@ -94,12 +104,17 @@ pub async fn print_direct_messages( } else { for m in dm.iter() { let message = m.0.get_inner_message_kind(); - let date = DateTime::from_timestamp(m.1 as i64, 0).unwrap(); - if message.id.is_some() { + let date = match DateTime::from_timestamp(m.1 as i64, 0) { + Some(dt) => dt, + None => { + println!("Error: Invalid timestamp {}", m.1); + continue; + } + }; + if let Some(order_id) = message.id { println!( "Mostro sent you this message for order id: {} at {}", - m.0.get_inner_message_kind().id.unwrap(), - date + order_id, date ); } if let Some(payload) = &message.payload { @@ -129,17 +144,20 @@ pub async fn print_direct_messages( println!(); } Payload::Order(new_order) if message.action == Action::NewOrder => { - if new_order.id.is_some() { - let db_order = - Order::get_by_id(pool, &new_order.id.unwrap().to_string()).await; + if let Some(order_id) = new_order.id { + let db_order = Order::get_by_id(pool, &order_id.to_string()).await; if db_order.is_err() { - let trade_index = message.trade_index.unwrap(); - let trade_keys = User::get_trade_keys(pool, trade_index).await?; - let _ = Order::new(pool, new_order.clone(), &trade_keys, None) - .await - .map_err(|e| { - anyhow::anyhow!("Failed to create DB order: {:?}", e) - })?; + if let Some(trade_index) = message.trade_index { + let trade_keys = + User::get_trade_keys(pool, trade_index).await?; + let _ = Order::new(pool, new_order.clone(), &trade_keys, None) + .await + .map_err(|e| { + anyhow::anyhow!("Failed to create DB order: {:?}", e) + })?; + } else { + println!("Warning: No trade_index found for new order"); + } } } println!(); From 650f79d3c7dda2d63807b93c4a036a7fdc719f2e Mon Sep 17 00:00:00 2001 From: arkanoider Date: Sun, 14 Sep 2025 14:41:07 +0200 Subject: [PATCH 11/19] refactor: removed many unwraps and some logic improvement --- src/cli.rs | 19 ++++++------- src/cli/dm_to_user.rs | 9 ++++-- src/cli/get_dm.rs | 60 +++++++++++++++++++++++++--------------- src/cli/list_disputes.rs | 1 - src/cli/list_orders.rs | 1 - src/cli/new_order.rs | 3 +- src/cli/rate_user.rs | 22 +++++++++------ src/parser/disputes.rs | 5 +++- src/parser/dms.rs | 5 +--- src/parser/orders.rs | 50 +++++++++++++++++++++------------ src/util.rs | 24 +++++++++++++--- 11 files changed, 126 insertions(+), 73 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index feeff3c..232ff9e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -36,7 +36,6 @@ use clap::{Parser, Subcommand}; use mostro_core::prelude::*; use nostr_sdk::prelude::*; use sqlx::SqlitePool; -use std::sync::OnceLock; use std::{ env::{set_var, var}, str::FromStr, @@ -44,10 +43,7 @@ use std::{ use take_dispute::*; use uuid::Uuid; -pub static IDENTITY_KEYS: OnceLock = OnceLock::new(); -pub static POOL: OnceLock = OnceLock::new(); -pub static TRADE_KEY: OnceLock<(Keys, i64)> = OnceLock::new(); - +#[derive(Debug)] pub struct Context { pub client: Client, pub identity_keys: Keys, @@ -376,19 +372,16 @@ async fn init_context(cli: &Cli) -> Result { // Initialize database pool let pool = connect().await?; - POOL.get_or_init(|| pool.clone()); // Get identity keys let identity_keys = User::get_identity_keys(&pool) .await .map_err(|e| anyhow::anyhow!("Failed to get identity keys: {}", e))?; - IDENTITY_KEYS.get_or_init(|| identity_keys.clone()); // Get trade keys let (trade_keys, trade_index) = User::get_next_trade_keys(&pool) .await .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?; - TRADE_KEY.get_or_init(|| (trade_keys.clone(), trade_index)); // Load Mostro admin keys if available (optional) let mostro_keys = if let Ok(k) = std::env::var("NSEC_PRIVKEY") { @@ -450,8 +443,14 @@ impl Commands { order_id, message, } => { - execute_dm_to_user(PublicKey::from_str(pubkey)?, &ctx.client, order_id, message) - .await + execute_dm_to_user( + PublicKey::from_str(pubkey)?, + &ctx.client, + order_id, + message, + &ctx.pool, + ) + .await } Commands::AdmSendDm { pubkey, message } => { execute_adm_send_dm(PublicKey::from_str(pubkey)?, &ctx.client, message).await diff --git a/src/cli/dm_to_user.rs b/src/cli/dm_to_user.rs index 9425f86..2288e80 100644 --- a/src/cli/dm_to_user.rs +++ b/src/cli/dm_to_user.rs @@ -1,6 +1,7 @@ use crate::{db::Order, util::send_gift_wrap_dm}; use anyhow::Result; use nostr_sdk::prelude::*; +use sqlx::SqlitePool; use uuid::Uuid; pub async fn execute_dm_to_user( @@ -8,17 +9,19 @@ pub async fn execute_dm_to_user( client: &Client, order_id: &Uuid, message: &str, + pool: &SqlitePool, ) -> Result<()> { - let pool = crate::db::connect().await?; - - let order = Order::get_by_id(&pool, &order_id.to_string()) + // Get the order + let order = Order::get_by_id(pool, &order_id.to_string()) .await .map_err(|_| anyhow::anyhow!("order {} not found", order_id))?; + // Get the trade keys let trade_keys = match order.trade_keys.as_ref() { Some(trade_keys) => Keys::parse(trade_keys)?, None => anyhow::bail!("No trade_keys found for this order"), }; + // Send the DM println!( "SENDING DM with trade keys: {}", trade_keys.public_key().to_hex() diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index 6bb4e33..82475c8 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -1,12 +1,11 @@ use anyhow::Result; -use mostro_core::prelude::*; +use mostro_core::prelude::Message; use nostr_sdk::prelude::*; use sqlx::SqlitePool; use crate::{ - db::User, - parser::dms::{parse_dm_events, print_direct_messages}, - util::{create_filter, ListKind}, + parser::dms::print_direct_messages, + util::{fetch_events_list, Event, ListKind}, }; pub async fn execute_get_dm( @@ -17,26 +16,43 @@ pub async fn execute_get_dm( admin: bool, pool: &SqlitePool, ) -> Result<()> { - let mut dm: Vec<(Message, u64, PublicKey)> = Vec::new(); - if !admin { - for index in 1..=trade_index { - let keys = User::get_trade_keys(pool, index).await?; - let filter = create_filter(ListKind::DirectMessagesUser, keys.public_key()); - let fetched_events = client - .fetch_events(filter, std::time::Duration::from_secs(15)) - .await?; - let dm_temp = parse_dm_events(fetched_events, &keys).await; - dm.extend(dm_temp); - } + // Fetch the requested events + let all_fetched_events = if !admin { + fetch_events_list( + ListKind::DirectMessagesUser, + None, + None, + None, + mostro_keys, + trade_index, + pool, + client, + ) + .await? + // all_fetched_events.extend(fetched_events); } else { - let filter = create_filter(ListKind::DirectMessagesMostro, mostro_keys.public_key()); - let fetched_events = client - .fetch_events(filter, std::time::Duration::from_secs(15)) - .await?; - let dm_temp = parse_dm_events(fetched_events, mostro_keys).await; - dm.extend(dm_temp); + fetch_events_list( + ListKind::DirectMessagesMostro, + None, + None, + None, + mostro_keys, + trade_index, + pool, + client, + ) + .await? + // all_fetched_events.extend(fetched_events); + }; + + // Extract (Message, u64) tuples from Event::MessageTuple variants + let mut dm_events: Vec<(Message, u64)> = Vec::new(); + for event in all_fetched_events { + if let Event::MessageTuple(tuple) = event { + dm_events.push(*tuple); + } } - print_direct_messages(&dm, pool).await?; + print_direct_messages(&dm_events, pool).await?; Ok(()) } diff --git a/src/cli/list_disputes.rs b/src/cli/list_disputes.rs index 47c7056..02f7d0a 100644 --- a/src/cli/list_disputes.rs +++ b/src/cli/list_disputes.rs @@ -23,7 +23,6 @@ pub async fn execute_list_disputes( None, None, None, - mostro_key, mostro_keys, trade_index, pool, diff --git a/src/cli/list_orders.rs b/src/cli/list_orders.rs index 44bf24d..c0cbb24 100644 --- a/src/cli/list_orders.rs +++ b/src/cli/list_orders.rs @@ -63,7 +63,6 @@ pub async fn execute_list_orders( status_checked, upper_currency, kind_checked, - mostro_pubkey, mostro_keys, trade_index, pool, diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index 19fc439..51fe75a 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -89,7 +89,8 @@ pub async fn execute_new_order( let order_content = Payload::Order(small_order.clone()); // Print order preview - let ord_preview = print_order_preview(order_content.clone()).unwrap(); + let ord_preview = print_order_preview(order_content.clone()) + .map_err(|e| anyhow::anyhow!("Failed to generate order preview: {}", e))?; println!("{ord_preview}"); let mut user_input = String::new(); let _input = stdin(); diff --git a/src/cli/rate_user.rs b/src/cli/rate_user.rs index e9e8962..fea9e9c 100644 --- a/src/cli/rate_user.rs +++ b/src/cli/rate_user.rs @@ -8,6 +8,15 @@ const RATING_BOUNDARIES: [u8; 5] = [1, 2, 3, 4, 5]; use crate::{db::Order, util::send_message_sync}; +// Get the user rate +fn get_user_rate(rating: &u8) -> Result { + if let Some(rating) = RATING_BOUNDARIES.iter().find(|r| r == &rating) { + Ok(Payload::RatingUser(*rating)) + } else { + Err(anyhow::anyhow!("Rating must be in the range 1 - 5")) + } +} + pub async fn execute_rate_user( order_id: &Uuid, rating: &u8, @@ -17,24 +26,19 @@ pub async fn execute_rate_user( pool: &SqlitePool, ) -> Result<()> { // Check boundaries - let rating_content = if let Some(rating) = RATING_BOUNDARIES.iter().find(|r| r == &rating) { - Payload::RatingUser(*rating) - } else { - println!("Rating must be in the range 1 - 5"); - std::process::exit(0); - }; + let rating_content = get_user_rate(rating)?; + // Get the trade keys let trade_keys = if let Ok(order_to_vote) = Order::get_by_id(pool, &order_id.to_string()).await { match order_to_vote.trade_keys.as_ref() { Some(trade_keys) => Keys::parse(trade_keys)?, None => { - anyhow::bail!("No trade_keys found for this order"); + return Err(anyhow::anyhow!("No trade_keys found for this order")); } } } else { - println!("order {} not found", order_id); - std::process::exit(0) + return Err(anyhow::anyhow!("order {} not found", order_id)); }; // Create rating message of counterpart diff --git a/src/parser/disputes.rs b/src/parser/disputes.rs index db2a3f0..35bce8c 100644 --- a/src/parser/disputes.rs +++ b/src/parser/disputes.rs @@ -104,7 +104,10 @@ pub fn print_disputes_table(disputes_table: Vec) -> Result { let r = Row::from(vec![ Cell::new(single_dispute.id).set_alignment(CellAlignment::Center), Cell::new(single_dispute.status.to_string()).set_alignment(CellAlignment::Center), - Cell::new(date.unwrap()), + Cell::new( + date.map(|d| d.to_string()) + .unwrap_or_else(|| "Invalid date".to_string()), + ), ]); rows.push(r); } diff --git a/src/parser/dms.rs b/src/parser/dms.rs index 6ae2b44..9855157 100644 --- a/src/parser/dms.rs +++ b/src/parser/dms.rs @@ -93,10 +93,7 @@ pub async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message, u64 direct_messages } -pub async fn print_direct_messages( - dm: &[(Message, u64, PublicKey)], - pool: &SqlitePool, -) -> Result<()> { +pub async fn print_direct_messages(dm: &[(Message, u64)], pool: &SqlitePool) -> Result<()> { if dm.is_empty() { println!(); println!("No new messages"); diff --git a/src/parser/orders.rs b/src/parser/orders.rs index 1fcaef1..13a4424 100644 --- a/src/parser/orders.rs +++ b/src/parser/orders.rs @@ -28,9 +28,9 @@ pub fn parse_orders_events( continue; } if let Ok(mut order) = order { - info!("Found Order id : {:?}", order.id.unwrap()); - - if order.id.is_none() { + if let Some(order_id) = order.id { + info!("Found Order id : {:?}", order_id); + } else { info!("Order ID is none"); continue; } @@ -141,11 +141,12 @@ pub fn print_order_preview(ord: Payload) -> Result { if single_order.min_amount.is_none() && single_order.max_amount.is_none() { Cell::new(single_order.fiat_amount.to_string()).set_alignment(CellAlignment::Center) } else { - let range_str = format!( - "{}-{}", - single_order.min_amount.unwrap(), - single_order.max_amount.unwrap() - ); + let range_str = match (single_order.min_amount, single_order.max_amount) { + (Some(min), Some(max)) => format!("{}-{}", min, max), + (Some(min), None) => format!("{}-?", min), + (None, Some(max)) => format!("?-{}", max), + (None, None) => "?".to_string(), + }; Cell::new(range_str).set_alignment(CellAlignment::Center) }, Cell::new(single_order.payment_method.to_string()).set_alignment(CellAlignment::Center), @@ -243,9 +244,20 @@ pub fn print_orders_table(orders_table: Vec) -> Result { } else { Cell::new("BUY/SELL").set_alignment(CellAlignment::Center) }, - Cell::new(single_order.id.unwrap()).set_alignment(CellAlignment::Center), - Cell::new(single_order.status.unwrap().to_string()) - .set_alignment(CellAlignment::Center), + Cell::new( + single_order + .id + .map(|id| id.to_string()) + .unwrap_or_else(|| "N/A".to_string()), + ) + .set_alignment(CellAlignment::Center), + Cell::new( + single_order + .status + .unwrap_or(mostro_core::order::Status::Active) + .to_string(), + ) + .set_alignment(CellAlignment::Center), if single_order.amount == 0 { Cell::new("market price").set_alignment(CellAlignment::Center) } else { @@ -257,16 +269,20 @@ pub fn print_orders_table(orders_table: Vec) -> Result { Cell::new(single_order.fiat_amount.to_string()) .set_alignment(CellAlignment::Center) } else { - let range_str = format!( - "{}-{}", - single_order.min_amount.unwrap(), - single_order.max_amount.unwrap() - ); + let range_str = match (single_order.min_amount, single_order.max_amount) { + (Some(min), Some(max)) => format!("{}-{}", min, max), + (Some(min), None) => format!("{}-?", min), + (None, Some(max)) => format!("?-{}", max), + (None, None) => "?".to_string(), + }; Cell::new(range_str).set_alignment(CellAlignment::Center) }, Cell::new(single_order.payment_method.to_string()) .set_alignment(CellAlignment::Center), - Cell::new(date.unwrap()), + Cell::new( + date.map(|d| d.to_string()) + .unwrap_or_else(|| "Invalid date".to_string()), + ), ]); rows.push(r); } diff --git a/src/util.rs b/src/util.rs index 71fc57b..2067eac 100644 --- a/src/util.rs +++ b/src/util.rs @@ -28,6 +28,7 @@ pub enum ListKind { Disputes, DirectMessagesUser, DirectMessagesMostro, + PrivateDirectMessagesUser, } async fn send_gift_wrap_dm_internal( @@ -565,6 +566,21 @@ pub fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { .since(fake_timestamp) } ListKind::DirectMessagesUser => { + // We use a fake timestamp to thwart time-analysis attacks + let fake_since = 2880; + let fake_since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(fake_since)) + .unwrap() + .timestamp() as u64; + + let fake_timestamp = Timestamp::from(fake_since_time); + + Filter::new() + .kind(nostr_sdk::Kind::GiftWrap) + .pubkey(pubkey) + .since(fake_timestamp) + } + ListKind::PrivateDirectMessagesUser => { // Get the timestamp from the since parameter let fake_since = 2880; let fake_since_time = chrono::Utc::now() @@ -575,7 +591,7 @@ pub fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { // Create filter for fetching direct messages Filter::new() - .kind(nostr_sdk::Kind::GiftWrap) + .kind(nostr_sdk::Kind::PrivateDirectMessage) .pubkey(pubkey) .since(fake_timestamp) } @@ -588,7 +604,6 @@ pub async fn fetch_events_list( status: Option, currency: Option, kind: Option, - mostro_pubkey: PublicKey, mostro_keys: &Keys, trade_index: i64, pool: &SqlitePool, @@ -596,7 +611,7 @@ pub async fn fetch_events_list( ) -> Result> { match list_kind { ListKind::Orders => { - let filters = create_filter(list_kind, mostro_pubkey); + let filters = create_filter(list_kind, mostro_keys.public_key); let fetched_events = client .fetch_events(filters, Duration::from_secs(15)) .await?; @@ -635,13 +650,14 @@ pub async fn fetch_events_list( .collect()) } ListKind::Disputes => { - let filters = create_filter(list_kind, mostro_pubkey); + let filters = create_filter(list_kind, mostro_keys.public_key); let fetched_events = client .fetch_events(filters, Duration::from_secs(15)) .await?; let disputes = parse_dispute_events(fetched_events); Ok(disputes.into_iter().map(Event::Dispute).collect()) } + _ => Err(anyhow::anyhow!("Invalid list kind")), } } From a25437fe24714de2fa71c0040a4d4be199c301c4 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Sun, 14 Sep 2025 20:28:13 +0200 Subject: [PATCH 12/19] refactor: fixing ideas on dm --- src/cli.rs | 36 ++++++-------- src/cli/get_dm.rs | 6 +-- src/cli/list_disputes.rs | 1 + src/cli/list_orders.rs | 11 ++-- src/cli/rate_user.rs | 2 +- src/nip33.rs | 27 +++++----- src/parser/orders.rs | 105 ++++++++++++++++++--------------------- src/util.rs | 43 ++++++++++------ 8 files changed, 115 insertions(+), 116 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 232ff9e..d004e9d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -294,17 +294,17 @@ fn get_env_var(cli: &Cli) { pretty_env_logger::init(); } - if cli.mostropubkey.is_some() { - set_var("MOSTRO_PUBKEY", cli.mostropubkey.clone().unwrap()); + if let Some(ref mostro_pubkey) = cli.mostropubkey { + set_var("MOSTRO_PUBKEY", mostro_pubkey.clone()); } let _pubkey = var("MOSTRO_PUBKEY").expect("$MOSTRO_PUBKEY env var needs to be set"); - if cli.relays.is_some() { - set_var("RELAYS", cli.relays.clone().unwrap()); + if let Some(ref relays) = cli.relays { + set_var("RELAYS", relays.clone()); } - if cli.pow.is_some() { - set_var("POW", cli.pow.clone().unwrap()); + if let Some(ref pow) = cli.pow { + set_var("POW", pow.clone()); } if cli.secret { @@ -315,9 +315,6 @@ fn get_env_var(cli: &Cli) { // Check range with two values value fn check_fiat_range(s: &str) -> Result<(i64, Option)> { if s.contains('-') { - let min: i64; - let max: i64; - // Get values from CLI let values: Vec<&str> = s.split('-').collect(); @@ -327,17 +324,12 @@ fn check_fiat_range(s: &str) -> Result<(i64, Option)> { }; // Get ranged command - if let Err(e) = values[0].parse::() { - return Err(e.into()); - } else { - min = values[0].parse().unwrap(); - } - - if let Err(e) = values[1].parse::() { - return Err(e.into()); - } else { - max = values[1].parse().unwrap(); - } + let min = values[0] + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid min value: {}", e))?; + let max = values[1] + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid max value: {}", e))?; // Check min below max if min >= max { @@ -565,7 +557,7 @@ impl Commands { // DM retrieval commands Commands::GetDm { since } => { execute_get_dm( - since, + Some(since), ctx.trade_index, &ctx.mostro_keys, &ctx.client, @@ -579,7 +571,7 @@ impl Commands { } Commands::GetAdminDm { since } => { execute_get_dm( - since, + Some(since), ctx.trade_index, &ctx.mostro_keys, &ctx.client, diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index 82475c8..4618b69 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -9,7 +9,7 @@ use crate::{ }; pub async fn execute_get_dm( - _since: &i64, + since: Option<&i64>, trade_index: i64, mostro_keys: &Keys, client: &Client, @@ -25,11 +25,11 @@ pub async fn execute_get_dm( None, mostro_keys, trade_index, + since, pool, client, ) .await? - // all_fetched_events.extend(fetched_events); } else { fetch_events_list( ListKind::DirectMessagesMostro, @@ -38,11 +38,11 @@ pub async fn execute_get_dm( None, mostro_keys, trade_index, + since, pool, client, ) .await? - // all_fetched_events.extend(fetched_events); }; // Extract (Message, u64) tuples from Event::MessageTuple variants diff --git a/src/cli/list_disputes.rs b/src/cli/list_disputes.rs index 02f7d0a..0d4c8ae 100644 --- a/src/cli/list_disputes.rs +++ b/src/cli/list_disputes.rs @@ -25,6 +25,7 @@ pub async fn execute_list_disputes( None, mostro_keys, trade_index, + None, pool, client, ) diff --git a/src/cli/list_orders.rs b/src/cli/list_orders.rs index c0cbb24..ed4f1a6 100644 --- a/src/cli/list_orders.rs +++ b/src/cli/list_orders.rs @@ -19,10 +19,9 @@ pub async fn execute_list_orders( ) -> Result<()> { // Used to get upper currency string to check against a list of tickers let mut upper_currency: Option = None; - let mut status_checked: Option = Some( - Status::from_str("pending") - .map_err(|e| anyhow::anyhow!("Invalid default status 'pending': {:?}", e))?, - ); + // Default status is pending + let mut status_checked: Option = Some(Status::Pending); + // Default kind is none let mut kind_checked: Option = None; // New check against strings @@ -33,10 +32,11 @@ pub async fn execute_list_orders( ); } + // Print status requested if let Some(status) = &status_checked { println!("You are searching orders with status {:?}", status); } - // New check against strings + // New check against strings for kind if let Some(k) = kind { kind_checked = Some( mostro_core::order::Kind::from_str(k) @@ -65,6 +65,7 @@ pub async fn execute_list_orders( kind_checked, mostro_keys, trade_index, + None, pool, client, ) diff --git a/src/cli/rate_user.rs b/src/cli/rate_user.rs index fea9e9c..ff7b6b9 100644 --- a/src/cli/rate_user.rs +++ b/src/cli/rate_user.rs @@ -61,5 +61,5 @@ pub async fn execute_rate_user( ) .await?; - std::process::exit(0); + Ok(()) } diff --git a/src/nip33.rs b/src/nip33.rs index e566888..3909132 100644 --- a/src/nip33.rs +++ b/src/nip33.rs @@ -62,23 +62,26 @@ pub fn dispute_from_tags(tags: Tags) -> Result { let mut dispute = Dispute::default(); for tag in tags { let t = tag.to_vec(); - let v = t.get(1).unwrap().as_str(); - match t.first().unwrap().as_str() { + + // Check if tag has at least 2 elements + if t.len() < 2 { + continue; + } + + let key = t.first().map(|s| s.as_str()).unwrap_or(""); + let value = t.get(1).map(|s| s.as_str()).unwrap_or(""); + + match key { "d" => { - let id = t.get(1).unwrap().as_str().parse::(); - let id = match id { - core::result::Result::Ok(id) => id, - Err(_) => return Err(anyhow::anyhow!("Invalid dispute id")), - }; + let id = value + .parse::() + .map_err(|_| anyhow::anyhow!("Invalid dispute id"))?; dispute.id = id; } "s" => { - let status = match DisputeStatus::from_str(v) { - core::result::Result::Ok(status) => status, - Err(_) => return Err(anyhow::anyhow!("Invalid dispute status")), - }; - + let status = DisputeStatus::from_str(value) + .map_err(|_| anyhow::anyhow!("Invalid dispute status"))?; dispute.status = status.to_string(); } diff --git a/src/parser/orders.rs b/src/parser/orders.rs index 13a4424..7ccece3 100644 --- a/src/parser/orders.rs +++ b/src/parser/orders.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::util::Event; use anyhow::Result; use chrono::DateTime; @@ -6,6 +8,7 @@ use comfy_table::*; use log::{error, info}; use mostro_core::prelude::*; use nostr_sdk::prelude::*; +use uuid::Uuid; use crate::nip33::order_from_tags; @@ -15,73 +18,59 @@ pub fn parse_orders_events( status: Option, kind: Option, ) -> Vec { - // Extracted Orders List - let mut complete_events_list = Vec::::new(); - let mut requested_orders_list = Vec::::new(); + // HashMap to store the latest order by id + let mut latest_by_id: HashMap = HashMap::new(); - // Scan events to extract all orders for event in events.iter() { - let order = order_from_tags(event.tags.clone()); - - if order.is_err() { - error!("{order:?}"); - continue; - } - if let Ok(mut order) = order { - if let Some(order_id) = order.id { - info!("Found Order id : {:?}", order_id); - } else { - info!("Order ID is none"); + // Get order from tags + let mut order = match order_from_tags(event.tags.clone()) { + Ok(o) => o, + Err(e) => { + error!("{e:?}"); continue; } - - // Check if order kind is none - if order.kind.is_none() { - info!("Order kind is none"); + }; + // Get order id + let order_id = match order.id { + Some(id) => id, + None => { + info!("Order ID is none"); continue; } - - // Check if order status is none - if let Some(filter_status) = status { - if order.status != Some(filter_status) { - continue; - } - } - // Check if order fiat code is none - if let Some(ref curr) = currency { - if order.fiat_code != *curr { - continue; - } - } - - // Get created at field from Nostr event - if let Some(ref k) = kind { - if order.kind.as_ref() != Some(k) { - continue; + }; + // Check if order kind is none + if order.kind.is_none() { + info!("Order kind is none"); + continue; + } + // Set created at + order.created_at = Some(event.created_at.as_u64() as i64); + // Update latest order by id + latest_by_id + .entry(order_id) + .and_modify(|existing| { + let new_ts = order.created_at.unwrap_or(0); + let old_ts = existing.created_at.unwrap_or(0); + if new_ts > old_ts { + *existing = order.clone(); } - } + }) + .or_insert(order); + } - // Get created at field from Nostr event - order.created_at = Some(event.created_at.as_u64() as i64); - complete_events_list.push(order.clone()); + let mut requested: Vec = latest_by_id + .into_values() + .filter(|o| status.map(|s| o.status == Some(s)).unwrap_or(true)) + .filter(|o| currency.as_ref().map(|c| o.fiat_code == *c).unwrap_or(true)) + .filter(|o| { + kind.as_ref() + .map(|k| o.kind.as_ref() == Some(k)) + .unwrap_or(true) + }) + .collect(); - // Add just requested orders requested by filtering - requested_orders_list.push(order); - } - // Order all element ( orders ) received to filter - discard disaligned messages - // if an order has an older message with the state we received is discarded for the latest one - requested_orders_list.retain(|keep| { - !complete_events_list - .iter() - .any(|x| x.id == keep.id && x.created_at > keep.created_at) - }); - // Sort by id to remove duplicates - requested_orders_list.sort_by(|a, b| b.id.cmp(&a.id)); - requested_orders_list.dedup_by(|a, b| a.id == b.id); - } - // Finally sort list by creation time - requested_orders_list.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - requested_orders_list + requested.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + requested } pub fn print_order_preview(ord: Payload) -> Result { diff --git a/src/util.rs b/src/util.rs index 2067eac..5c31728 100644 --- a/src/util.rs +++ b/src/util.rs @@ -495,7 +495,7 @@ pub async fn get_direct_messages_from_trade_keys( for trade_key_hex in trade_keys_hex { if let Ok(public_key) = PublicKey::from_hex(&trade_key_hex) { // Create filter for fetching direct messages - let filter = create_filter(ListKind::DirectMessagesUser, public_key); + let filter = create_filter(ListKind::DirectMessagesUser, public_key, None); let events = client.fetch_events(filter, Duration::from_secs(15)).await?; // Parse events without keys since we only have the public key // We'll need to handle this differently - let's just collect the events for now @@ -513,7 +513,7 @@ pub async fn get_direct_messages_from_trade_keys( Ok(all_messages) } -pub fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { +pub fn create_filter(list_kind: ListKind, pubkey: PublicKey, since: Option<&u64>) -> Filter { match list_kind { ListKind::Orders => { let since_time = chrono::Utc::now() @@ -581,19 +581,23 @@ pub fn create_filter(list_kind: ListKind, pubkey: PublicKey) -> Filter { .since(fake_timestamp) } ListKind::PrivateDirectMessagesUser => { - // Get the timestamp from the since parameter - let fake_since = 2880; - let fake_since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(fake_since)) + // Get since from cli or use 30 minutes default + let since = if let Some(since) = since { + chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(*since as i64)) .unwrap() - .timestamp() as u64; - let fake_timestamp = Timestamp::from(fake_since_time); - - // Create filter for fetching direct messages + .timestamp() + } else { + chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(30)) + .unwrap() + .timestamp() + } as u64; + // Create filter for fetching privatedirect messages Filter::new() .kind(nostr_sdk::Kind::PrivateDirectMessage) .pubkey(pubkey) - .since(fake_timestamp) + .since(Timestamp::from(since)) } } } @@ -606,12 +610,13 @@ pub async fn fetch_events_list( kind: Option, mostro_keys: &Keys, trade_index: i64, + _since: Option<&i64>, pool: &SqlitePool, client: &Client, ) -> Result> { match list_kind { ListKind::Orders => { - let filters = create_filter(list_kind, mostro_keys.public_key); + let filters = create_filter(list_kind, mostro_keys.public_key, None); let fetched_events = client .fetch_events(filters, Duration::from_secs(15)) .await?; @@ -619,7 +624,7 @@ pub async fn fetch_events_list( Ok(orders.into_iter().map(Event::SmallOrder).collect()) } ListKind::DirectMessagesMostro => { - let filters = create_filter(list_kind, mostro_keys.public_key()); + let filters = create_filter(list_kind, mostro_keys.public_key(), None); let fetched_events = client .fetch_events(filters, Duration::from_secs(15)) .await?; @@ -629,11 +634,19 @@ pub async fn fetch_events_list( .map(|(message, timestamp, _)| Event::MessageTuple(Box::new((message, timestamp)))) .collect()) } + // ListKind::PrivateDirectMessagesUser => { + // let filters = create_filter(list_kind, mostro_keys.public_key, since); + // let fetched_events = client + // .fetch_events(filters, Duration::from_secs(15)) + // .await?; + // let direct_messages_mostro = parse_dm_events(fetched_events, mostro_keys).await; + + // } ListKind::DirectMessagesUser => { let mut direct_messages: Vec<(Message, u64)> = Vec::new(); for index in 1..=trade_index { let trade_key = User::get_trade_keys(pool, index).await?; - let filter = create_filter(ListKind::DirectMessagesUser, trade_key.public_key()); + let filter = create_filter(ListKind::DirectMessagesUser, trade_key.public_key(), None); let fetched_user_messages = client.fetch_events(filter, Duration::from_secs(15)).await?; let direct_messages_for_trade_key = @@ -650,7 +663,7 @@ pub async fn fetch_events_list( .collect()) } ListKind::Disputes => { - let filters = create_filter(list_kind, mostro_keys.public_key); + let filters = create_filter(list_kind, mostro_keys.public_key, None); let fetched_events = client .fetch_events(filters, Duration::from_secs(15)) .await?; From 7badb7bbcca49d9f6e57b8fb6d8abf5756dde3d2 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Sun, 14 Sep 2025 23:52:07 +0200 Subject: [PATCH 13/19] fix: some bugs on keys parameters fixed - will continue with the order flow --- src/cli.rs | 14 ++++- src/cli/get_dm.rs | 28 +++++----- src/cli/list_disputes.rs | 5 +- src/cli/list_orders.rs | 1 + src/cli/rate_user.rs | 12 +++-- src/cli/send_dm.rs | 8 +-- src/cli/take_dispute.rs | 42 +++++++++------ src/util.rs | 108 ++++++++++++++------------------------- 8 files changed, 105 insertions(+), 113 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index d004e9d..208640e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -170,6 +170,9 @@ pub enum Commands { #[arg(short, long)] #[clap(default_value_t = 30)] since: i64, + /// If true, get messages from counterparty, otherwise from Mostro + #[arg(short, long)] + from_user: bool, }, /// Get direct messages sent to any trade keys GetDmUser { @@ -184,6 +187,9 @@ pub enum Commands { #[arg(short, long)] #[clap(default_value_t = 30)] since: i64, + /// If true, get messages from counterparty, otherwise from Mostro + #[arg(short, long)] + from_user: bool, }, /// Send direct message to a user SendDm { @@ -555,13 +561,15 @@ impl Commands { } // DM retrieval commands - Commands::GetDm { since } => { + Commands::GetDm { since, from_user } => { execute_get_dm( Some(since), ctx.trade_index, + ctx.mostro_pubkey, &ctx.mostro_keys, &ctx.client, false, + from_user, &ctx.pool, ) .await @@ -569,13 +577,15 @@ impl Commands { Commands::GetDmUser { since } => { execute_get_dm_user(since, &ctx.client, &ctx.mostro_pubkey, &ctx.pool).await } - Commands::GetAdminDm { since } => { + Commands::GetAdminDm { since, from_user } => { execute_get_dm( Some(since), ctx.trade_index, + ctx.mostro_pubkey, &ctx.mostro_keys, &ctx.client, true, + from_user, &ctx.pool, ) .await diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index 4618b69..14b3704 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -11,31 +11,29 @@ use crate::{ pub async fn execute_get_dm( since: Option<&i64>, trade_index: i64, + mostro_pubkey: PublicKey, mostro_keys: &Keys, client: &Client, admin: bool, + from_user: &bool, pool: &SqlitePool, ) -> Result<()> { + // Get the list kind + let list_kind = match (admin, from_user) { + (true, true) => ListKind::PrivateDirectMessagesUser, + (true, false) => ListKind::DirectMessagesAdmin, + (false, true) => ListKind::PrivateDirectMessagesUser, + (false, false) => ListKind::DirectMessagesUser, + }; + // Fetch the requested events - let all_fetched_events = if !admin { - fetch_events_list( - ListKind::DirectMessagesUser, - None, - None, - None, - mostro_keys, - trade_index, - since, - pool, - client, - ) - .await? - } else { + let all_fetched_events = { fetch_events_list( - ListKind::DirectMessagesMostro, + list_kind, None, None, None, + &mostro_pubkey, mostro_keys, trade_index, since, diff --git a/src/cli/list_disputes.rs b/src/cli/list_disputes.rs index 0d4c8ae..7ae1fd6 100644 --- a/src/cli/list_disputes.rs +++ b/src/cli/list_disputes.rs @@ -6,7 +6,7 @@ use crate::parser::disputes::print_disputes_table; use crate::util::{fetch_events_list, ListKind}; pub async fn execute_list_disputes( - mostro_key: PublicKey, + mostro_pubkey: PublicKey, mostro_keys: &Keys, trade_index: i64, pool: &SqlitePool, @@ -14,7 +14,7 @@ pub async fn execute_list_disputes( ) -> Result<()> { println!( "Requesting disputes from mostro pubId - {}", - mostro_key.clone() + mostro_pubkey.clone() ); // Get orders from relays @@ -23,6 +23,7 @@ pub async fn execute_list_disputes( None, None, None, + &mostro_pubkey, mostro_keys, trade_index, None, diff --git a/src/cli/list_orders.rs b/src/cli/list_orders.rs index ed4f1a6..1602c07 100644 --- a/src/cli/list_orders.rs +++ b/src/cli/list_orders.rs @@ -63,6 +63,7 @@ pub async fn execute_list_orders( status_checked, upper_currency, kind_checked, + &mostro_pubkey, mostro_keys, trade_index, None, diff --git a/src/cli/rate_user.rs b/src/cli/rate_user.rs index ff7b6b9..e3de6a2 100644 --- a/src/cli/rate_user.rs +++ b/src/cli/rate_user.rs @@ -6,7 +6,7 @@ use uuid::Uuid; const RATING_BOUNDARIES: [u8; 5] = [1, 2, 3, 4, 5]; -use crate::{db::Order, util::send_message_sync}; +use crate::{db::Order, util::send_dm}; // Get the user rate fn get_user_rate(rating: &u8) -> Result { @@ -48,15 +48,17 @@ pub async fn execute_rate_user( None, Action::RateUser, Some(rating_content), - ); + ) + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - send_message_sync( + send_dm( client, Some(identity_keys), &trade_keys, - mostro_key, + &mostro_key, rate_message, - true, + None, false, ) .await?; diff --git a/src/cli/send_dm.rs b/src/cli/send_dm.rs index b60efb3..f07a142 100644 --- a/src/cli/send_dm.rs +++ b/src/cli/send_dm.rs @@ -1,4 +1,4 @@ -use crate::{db::Order, util::send_message_sync}; +use crate::{db::Order, util::send_dm}; use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; @@ -15,7 +15,9 @@ pub async fn execute_send_dm( None, Action::SendDm, Some(Payload::TextMessage(message.to_string())), - ); + ) + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; let pool = crate::db::connect().await?; @@ -32,7 +34,7 @@ pub async fn execute_send_dm( std::process::exit(0) }; - send_message_sync(client, None, &trade_keys, receiver, message, true, true).await?; + send_dm(client, None, &trade_keys, &receiver, message, None, false).await?; Ok(()) } diff --git a/src/cli/take_dispute.rs b/src/cli/take_dispute.rs index 61bb2e3..6cdbe0e 100644 --- a/src/cli/take_dispute.rs +++ b/src/cli/take_dispute.rs @@ -3,7 +3,7 @@ use mostro_core::prelude::*; use nostr_sdk::prelude::*; use uuid::Uuid; -use crate::util::send_message_sync; +use crate::util::send_dm; pub async fn execute_admin_add_solver( npubkey: &str, @@ -24,15 +24,17 @@ pub async fn execute_admin_add_solver( None, Action::AdminAddSolver, Some(Payload::TextMessage(npubkey.to_string())), - ); + ) + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - send_message_sync( + send_dm( client, Some(identity_keys), trade_keys, - mostro_key, + &mostro_key, take_dispute_message, - true, + None, false, ) .await?; @@ -54,17 +56,19 @@ pub async fn execute_admin_cancel_dispute( ); // Create takebuy message let take_dispute_message = - Message::new_dispute(Some(*dispute_id), None, None, Action::AdminCancel, None); + Message::new_dispute(Some(*dispute_id), None, None, Action::AdminCancel, None) + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; println!("identity_keys: {:?}", identity_keys.public_key.to_string()); - send_message_sync( + send_dm( client, Some(identity_keys), trade_keys, - mostro_key, + &mostro_key, take_dispute_message, - true, + None, false, ) .await?; @@ -86,17 +90,19 @@ pub async fn execute_admin_settle_dispute( ); // Create takebuy message let take_dispute_message = - Message::new_dispute(Some(*dispute_id), None, None, Action::AdminSettle, None); + Message::new_dispute(Some(*dispute_id), None, None, Action::AdminSettle, None) + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; println!("identity_keys: {:?}", identity_keys.public_key.to_string()); - send_message_sync( + send_dm( client, Some(identity_keys), trade_keys, - mostro_key, + &mostro_key, take_dispute_message, - true, + None, false, ) .await?; @@ -123,17 +129,19 @@ pub async fn execute_take_dispute( None, Action::AdminTakeDispute, None, - ); + ) + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; println!("identity_keys: {:?}", identity_keys.public_key.to_string()); - send_message_sync( + send_dm( client, Some(identity_keys), trade_keys, - mostro_key, + &mostro_key, take_dispute_message, - true, + None, false, ) .await?; diff --git a/src/util.rs b/src/util.rs index 5c31728..77750e8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -27,7 +27,7 @@ pub enum ListKind { Orders, Disputes, DirectMessagesUser, - DirectMessagesMostro, + DirectMessagesAdmin, PrivateDirectMessagesUser, } @@ -434,37 +434,6 @@ pub async fn connect_nostr() -> Result { Ok(client) } -pub async fn send_message_sync( - client: &Client, - identity_keys: Option<&Keys>, - trade_keys: &Keys, - receiver_pubkey: PublicKey, - message: Message, - _wait_for_dm: bool, - to_user: bool, -) -> Result<()> { - let message_json = message - .as_json() - .map_err(|_| Error::msg("Failed to serialize message"))?; - // Send dm to receiver pubkey - println!( - "SENDING DM with trade keys: {:?}", - trade_keys.public_key().to_hex() - ); - send_dm( - client, - identity_keys, - trade_keys, - &receiver_pubkey, - message_json, - None, - to_user, - ) - .await?; - - Ok(()) -} - pub async fn get_direct_messages_from_trade_keys( client: &Client, trade_keys_hex: Vec, @@ -550,22 +519,7 @@ pub fn create_filter(list_kind: ListKind, pubkey: PublicKey, since: Option<&u64> ) .kind(nostr_sdk::Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND)) } - ListKind::DirectMessagesMostro => { - // We use a fake timestamp to thwart time-analysis attacks - let fake_since = 2880; - let fake_since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(fake_since)) - .unwrap() - .timestamp() as u64; - - let fake_timestamp = Timestamp::from(fake_since_time); - - Filter::new() - .kind(nostr_sdk::Kind::GiftWrap) - .pubkey(pubkey) - .since(fake_timestamp) - } - ListKind::DirectMessagesUser => { + ListKind::DirectMessagesAdmin | ListKind::DirectMessagesUser => { // We use a fake timestamp to thwart time-analysis attacks let fake_since = 2880; let fake_since_time = chrono::Utc::now() @@ -584,14 +538,14 @@ pub fn create_filter(list_kind: ListKind, pubkey: PublicKey, since: Option<&u64> // Get since from cli or use 30 minutes default let since = if let Some(since) = since { chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(*since as i64)) - .unwrap() - .timestamp() + .checked_sub_signed(chrono::Duration::minutes(*since as i64)) + .unwrap() + .timestamp() } else { chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(30)) - .unwrap() - .timestamp() + .checked_sub_signed(chrono::Duration::minutes(30)) + .unwrap() + .timestamp() } as u64; // Create filter for fetching privatedirect messages Filter::new() @@ -608,6 +562,7 @@ pub async fn fetch_events_list( status: Option, currency: Option, kind: Option, + mostro_pubkey: &PublicKey, mostro_keys: &Keys, trade_index: i64, _since: Option<&i64>, @@ -616,15 +571,15 @@ pub async fn fetch_events_list( ) -> Result> { match list_kind { ListKind::Orders => { - let filters = create_filter(list_kind, mostro_keys.public_key, None); + let filters = create_filter(list_kind, *mostro_pubkey, None); let fetched_events = client .fetch_events(filters, Duration::from_secs(15)) .await?; let orders = parse_orders_events(fetched_events, currency, status, kind); Ok(orders.into_iter().map(Event::SmallOrder).collect()) } - ListKind::DirectMessagesMostro => { - let filters = create_filter(list_kind, mostro_keys.public_key(), None); + ListKind::DirectMessagesAdmin => { + let filters = create_filter(list_kind, *mostro_pubkey, None); let fetched_events = client .fetch_events(filters, Duration::from_secs(15)) .await?; @@ -634,19 +589,36 @@ pub async fn fetch_events_list( .map(|(message, timestamp, _)| Event::MessageTuple(Box::new((message, timestamp)))) .collect()) } - // ListKind::PrivateDirectMessagesUser => { - // let filters = create_filter(list_kind, mostro_keys.public_key, since); - // let fetched_events = client - // .fetch_events(filters, Duration::from_secs(15)) - // .await?; - // let direct_messages_mostro = parse_dm_events(fetched_events, mostro_keys).await; - - // } + ListKind::PrivateDirectMessagesUser => { + let mut direct_messages: Vec<(Message, u64)> = Vec::new(); + for index in 1..=trade_index { + let trade_key = User::get_trade_keys(pool, index).await?; + let filter = create_filter( + ListKind::PrivateDirectMessagesUser, + trade_key.public_key(), + None, + ); + let fetched_user_messages = + client.fetch_events(filter, Duration::from_secs(15)).await?; + let direct_messages_for_trade_key = + parse_dm_events(fetched_user_messages, &trade_key).await; + direct_messages.extend( + direct_messages_for_trade_key + .into_iter() + .map(|(message, timestamp, _)| (message, timestamp)), + ); + } + Ok(direct_messages + .into_iter() + .map(|t| Event::MessageTuple(Box::new(t))) + .collect()) + } ListKind::DirectMessagesUser => { let mut direct_messages: Vec<(Message, u64)> = Vec::new(); for index in 1..=trade_index { let trade_key = User::get_trade_keys(pool, index).await?; - let filter = create_filter(ListKind::DirectMessagesUser, trade_key.public_key(), None); + let filter = + create_filter(ListKind::DirectMessagesUser, trade_key.public_key(), None); let fetched_user_messages = client.fetch_events(filter, Duration::from_secs(15)).await?; let direct_messages_for_trade_key = @@ -663,14 +635,13 @@ pub async fn fetch_events_list( .collect()) } ListKind::Disputes => { - let filters = create_filter(list_kind, mostro_keys.public_key, None); + let filters = create_filter(list_kind, *mostro_pubkey, None); let fetched_events = client .fetch_events(filters, Duration::from_secs(15)) .await?; let disputes = parse_dispute_events(fetched_events); Ok(disputes.into_iter().map(Event::Dispute).collect()) } - _ => Err(anyhow::anyhow!("Invalid list kind")), } } @@ -690,7 +661,6 @@ pub fn get_mcli_path() -> String { fs::create_dir(&mcli_path).expect("Couldn't create mostro-cli directory in HOME"); println!("Directory {} created.", mcli_path); } - mcli_path } From 9e07206a09aaa810715271750a82a58ada0e92ea Mon Sep 17 00:00:00 2001 From: arkanoider Date: Mon, 15 Sep 2025 16:53:07 +0200 Subject: [PATCH 14/19] refactor: better management of context variable - cleaner approach for user --- src/cli.rs | 24 +++++++----------------- src/cli/get_dm.rs | 19 +++++-------------- src/cli/list_orders.rs | 17 +++++------------ src/cli/send_msg.rs | 35 +++++++++++++++-------------------- src/util.rs | 40 +++++++++++++++------------------------- 5 files changed, 47 insertions(+), 88 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 208640e..727a921 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -50,7 +50,7 @@ pub struct Context { pub trade_keys: Keys, pub trade_index: i64, pub pool: SqlitePool, - pub mostro_keys: Keys, + pub context_keys: Keys, pub mostro_pubkey: PublicKey, } @@ -381,12 +381,9 @@ async fn init_context(cli: &Cli) -> Result { .await .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?; - // Load Mostro admin keys if available (optional) - let mostro_keys = if let Ok(k) = std::env::var("NSEC_PRIVKEY") { - Keys::from_str(&k)? - } else { - println!("No Mostro admin keys found"); - Keys::generate() + // Load private key of user or admin - must be present in .env file + let context_keys = std::env::var("NSEC_PRIVKEY").map_err(|e| anyhow::anyhow!("NSEC_PRIVKEY not set: {}", e))? { + Keys::from_str(&k).map_err(|e| anyhow::anyhow!("Failed to get context keys: {}", e))? }; // Resolve Mostro pubkey from env (required for all flows) @@ -404,7 +401,7 @@ async fn init_context(cli: &Cli) -> Result { trade_keys, trade_index, pool, - mostro_keys, + context_keys, mostro_pubkey, }) } @@ -420,10 +417,7 @@ impl Commands { crate::util::run_simple_order_msg( self.clone(), order_id, - &ctx.identity_keys, - ctx.mostro_pubkey, - &ctx.client, - &ctx.pool, + &ctx ) .await } @@ -467,11 +461,7 @@ impl Commands { kind, currency, status, - ctx.mostro_pubkey, - &ctx.mostro_keys, - ctx.trade_index, - &ctx.pool, - &ctx.client, + &ctx ) .await } diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index 14b3704..671525f 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -4,19 +4,14 @@ use nostr_sdk::prelude::*; use sqlx::SqlitePool; use crate::{ - parser::dms::print_direct_messages, - util::{fetch_events_list, Event, ListKind}, + cli::Context, parser::dms::print_direct_messages, util::{fetch_events_list, Event, ListKind} }; pub async fn execute_get_dm( since: Option<&i64>, - trade_index: i64, - mostro_pubkey: PublicKey, - mostro_keys: &Keys, - client: &Client, admin: bool, from_user: &bool, - pool: &SqlitePool, + ctx :&Context ) -> Result<()> { // Get the list kind let list_kind = match (admin, from_user) { @@ -33,12 +28,8 @@ pub async fn execute_get_dm( None, None, None, - &mostro_pubkey, - mostro_keys, - trade_index, - since, - pool, - client, + &ctx, + since ) .await? }; @@ -51,6 +42,6 @@ pub async fn execute_get_dm( } } - print_direct_messages(&dm_events, pool).await?; + print_direct_messages(&dm_events, &ctx.pool).await?; Ok(()) } diff --git a/src/cli/list_orders.rs b/src/cli/list_orders.rs index 1602c07..caab880 100644 --- a/src/cli/list_orders.rs +++ b/src/cli/list_orders.rs @@ -5,17 +5,14 @@ use mostro_core::prelude::*; use nostr_sdk::prelude::*; use sqlx::SqlitePool; use std::str::FromStr; +use crate::cli::Context; #[allow(clippy::too_many_arguments)] pub async fn execute_list_orders( kind: &Option, currency: &Option, status: &Option, - mostro_pubkey: PublicKey, - mostro_keys: &Keys, - trade_index: i64, - pool: &SqlitePool, - client: &Client, + ctx: &Context, ) -> Result<()> { // Used to get upper currency string to check against a list of tickers let mut upper_currency: Option = None; @@ -55,7 +52,7 @@ pub async fn execute_list_orders( } } - println!("Requesting orders from mostro pubId - {}", mostro_pubkey); + println!("Requesting orders from mostro pubId - {}", &ctx.mostro_pubkey); // Get orders from relays let table_of_orders = fetch_events_list( @@ -63,12 +60,8 @@ pub async fn execute_list_orders( status_checked, upper_currency, kind_checked, - &mostro_pubkey, - mostro_keys, - trade_index, - None, - pool, - client, + &ctx, + since ) .await?; let table = print_orders_table(table_of_orders)?; diff --git a/src/cli/send_msg.rs b/src/cli/send_msg.rs index 019622f..0672667 100644 --- a/src/cli/send_msg.rs +++ b/src/cli/send_msg.rs @@ -1,4 +1,4 @@ -use crate::cli::Commands; +use crate::cli::{Commands, Context}; use crate::db::{Order, User}; use crate::util::{send_dm, wait_for_dm}; @@ -12,11 +12,8 @@ use uuid::Uuid; pub async fn execute_send_msg( command: Commands, order_id: Option, - identity_keys: Option<&Keys>, - mostro_key: PublicKey, - client: &Client, + ctx: &Context, text: Option<&str>, - pool: &SqlitePool, ) -> Result<()> { // Map CLI command to action let requested_action = match command { @@ -37,21 +34,21 @@ pub async fn execute_send_msg( "Sending {} command for order {:?} to mostro pubId {}", requested_action, order_id.as_ref(), - mostro_key + &ctx.mostro_pubkey ); // Determine payload let payload = match requested_action { - Action::FiatSent | Action::Release => create_next_trade_payload(pool, &order_id).await?, + Action::FiatSent | Action::Release => create_next_trade_payload(ctx, &order_id).await?, _ => text.map(|t| Payload::TextMessage(t.to_string())), }; // Update last trade index if next trade payload if let Some(Payload::NextTrade(_, trade_index)) = &payload { // Update last trade index - match User::get(pool).await { + match User::get(&ctx.pool).await { Ok(mut user) => { user.set_last_trade_index(*trade_index as i64); - if let Err(e) = user.save(pool).await { + if let Err(e) = user.save(&ctx.pool).await { println!("Failed to update user: {}", e); } } @@ -64,12 +61,10 @@ pub async fn execute_send_msg( // Create and send the message let message = Message::new_order(order_id, Some(request_id), None, requested_action, payload); - let idkey = identity_keys - .ok_or_else(|| anyhow::anyhow!("Identity keys are required"))? - .to_owned(); + let idkey = ctx.identity_keys.to_owned(); if let Some(order_id) = order_id { - let order = Order::get_by_id(pool, &order_id.to_string()).await?; + let order = Order::get_by_id(&ctx.pool, &order_id.to_string()).await?; if let Some(trade_keys_str) = order.trade_keys.clone() { let trade_keys = Keys::parse(&trade_keys_str)?; @@ -82,16 +77,16 @@ pub async fn execute_send_msg( let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); // Subscribe to gift wrap events - client.subscribe(subscription, Some(opts)).await?; + ctx.client.subscribe(subscription, Some(opts)).await?; // Send DM let message_json = message .as_json() .map_err(|e| anyhow::anyhow!("Failed to serialize message: {e}"))?; send_dm( - client, + &ctx.client, Some(&idkey), &trade_keys, - &mostro_key, + &ctx.mostro_pubkey, message_json, None, false, @@ -100,7 +95,7 @@ pub async fn execute_send_msg( .map_err(|e| anyhow::anyhow!("Failed to send DM: {e}"))?; // Wait for the DM to be sent from mostro - wait_for_dm(client, &trade_keys, request_id, None, Some(order), pool) + wait_for_dm(&ctx.client, &trade_keys, request_id, None, Some(order), &ctx.pool) .await .map_err(|e| anyhow::anyhow!("Failed to wait for DM: {e}"))?; } @@ -110,17 +105,17 @@ pub async fn execute_send_msg( } async fn create_next_trade_payload( - pool: &SqlitePool, + ctx: &Context, order_id: &Option, ) -> Result> { if let Some(order_id) = order_id { - let order = Order::get_by_id(pool, &order_id.to_string()).await?; + let order = Order::get_by_id(&ctx.pool, &order_id.to_string()).await?; if let (Some(_), Some(min_amount), Some(max_amount)) = (order.is_mine, order.min_amount, order.max_amount) { if max_amount - order.fiat_amount >= min_amount { - let (trade_keys, trade_index) = User::get_next_trade_keys(pool).await?; + let (trade_keys, trade_index) = User::get_next_trade_keys(&ctx.pool).await?; return Ok(Some(Payload::NextTrade( trade_keys.public_key().to_string(), trade_index.try_into()?, diff --git a/src/util.rs b/src/util.rs index 77750e8..cab301d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,5 @@ use crate::cli::send_msg::execute_send_msg; -use crate::cli::Commands; +use crate::cli::{Commands, Context}; use crate::db::{Order, User}; use crate::parser::{parse_dispute_events, parse_dm_events, parse_orders_events}; use anyhow::{Error, Result}; @@ -562,28 +562,24 @@ pub async fn fetch_events_list( status: Option, currency: Option, kind: Option, - mostro_pubkey: &PublicKey, - mostro_keys: &Keys, - trade_index: i64, + ctx: &Context, _since: Option<&i64>, - pool: &SqlitePool, - client: &Client, ) -> Result> { match list_kind { ListKind::Orders => { - let filters = create_filter(list_kind, *mostro_pubkey, None); - let fetched_events = client + let filters = create_filter(list_kind, ctx.mostro_pubkey, None); + let fetched_events = ctx.client .fetch_events(filters, Duration::from_secs(15)) .await?; let orders = parse_orders_events(fetched_events, currency, status, kind); Ok(orders.into_iter().map(Event::SmallOrder).collect()) } ListKind::DirectMessagesAdmin => { - let filters = create_filter(list_kind, *mostro_pubkey, None); - let fetched_events = client + let filters = create_filter(list_kind, ctx.mostro_pubkey, None); + let fetched_events = ctx.client .fetch_events(filters, Duration::from_secs(15)) .await?; - let direct_messages_mostro = parse_dm_events(fetched_events, mostro_keys).await; + let direct_messages_mostro = parse_dm_events(fetched_events, &ctx.context_keys).await; Ok(direct_messages_mostro .into_iter() .map(|(message, timestamp, _)| Event::MessageTuple(Box::new((message, timestamp)))) @@ -591,15 +587,15 @@ pub async fn fetch_events_list( } ListKind::PrivateDirectMessagesUser => { let mut direct_messages: Vec<(Message, u64)> = Vec::new(); - for index in 1..=trade_index { - let trade_key = User::get_trade_keys(pool, index).await?; + for index in 1..=ctx.trade_index { + let trade_key = User::get_trade_keys(&ctx.pool, index).await?; let filter = create_filter( ListKind::PrivateDirectMessagesUser, trade_key.public_key(), None, ); let fetched_user_messages = - client.fetch_events(filter, Duration::from_secs(15)).await?; + ctx.client.fetch_events(filter, Duration::from_secs(15)).await?; let direct_messages_for_trade_key = parse_dm_events(fetched_user_messages, &trade_key).await; direct_messages.extend( @@ -615,12 +611,12 @@ pub async fn fetch_events_list( } ListKind::DirectMessagesUser => { let mut direct_messages: Vec<(Message, u64)> = Vec::new(); - for index in 1..=trade_index { - let trade_key = User::get_trade_keys(pool, index).await?; + for index in 1..=ctx.trade_index { + let trade_key = User::get_trade_keys(&ctx.pool, index).await?; let filter = create_filter(ListKind::DirectMessagesUser, trade_key.public_key(), None); let fetched_user_messages = - client.fetch_events(filter, Duration::from_secs(15)).await?; + ctx.client.fetch_events(filter, Duration::from_secs(15)).await?; let direct_messages_for_trade_key = parse_dm_events(fetched_user_messages, &trade_key).await; direct_messages.extend( @@ -667,19 +663,13 @@ pub fn get_mcli_path() -> String { pub async fn run_simple_order_msg( command: Commands, order_id: &Uuid, - identity_keys: &Keys, - mostro_key: PublicKey, - client: &Client, - pool: &SqlitePool, + ctx: &Context, ) -> Result<()> { execute_send_msg( command, Some(*order_id), - Some(identity_keys), - mostro_key, - client, + &ctx, None, - pool, ) .await } From ceadbada95efa20505f80eb8c29a36a10986c2e3 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Mon, 15 Sep 2025 23:49:41 +0200 Subject: [PATCH 15/19] fix: compiling with new ctx struct - looking for fix on addinvoice response parse --- src/cli.rs | 107 +++++---------------------------------- src/cli/add_invoice.rs | 30 +++++++---- src/cli/get_dm.rs | 20 ++------ src/cli/get_dm_user.rs | 23 ++++----- src/cli/list_disputes.rs | 29 +++-------- src/cli/list_orders.rs | 13 ++--- src/cli/send_msg.rs | 14 +++-- src/cli/take_dispute.rs | 91 ++++++++++++++------------------- src/util.rs | 58 ++++++++++----------- 9 files changed, 138 insertions(+), 247 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 727a921..1db0edf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -382,9 +382,10 @@ async fn init_context(cli: &Cli) -> Result { .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?; // Load private key of user or admin - must be present in .env file - let context_keys = std::env::var("NSEC_PRIVKEY").map_err(|e| anyhow::anyhow!("NSEC_PRIVKEY not set: {}", e))? { - Keys::from_str(&k).map_err(|e| anyhow::anyhow!("Failed to get context keys: {}", e))? - }; + let context_keys = std::env::var("NSEC_PRIVKEY") + .map_err(|e| anyhow::anyhow!("NSEC_PRIVKEY not set: {}", e))? + .parse::() + .map_err(|e| anyhow::anyhow!("Failed to get context keys: {}", e))?; // Resolve Mostro pubkey from env (required for all flows) let mostro_pubkey = PublicKey::from_str( @@ -414,12 +415,7 @@ impl Commands { | Commands::Release { order_id } | Commands::Dispute { order_id } | Commands::Cancel { order_id } => { - crate::util::run_simple_order_msg( - self.clone(), - order_id, - &ctx - ) - .await + crate::util::run_simple_order_msg(self.clone(), order_id, ctx).await } // DM commands with pubkey parsing @@ -456,15 +452,7 @@ impl Commands { status, currency, kind, - } => { - execute_list_orders( - kind, - currency, - status, - &ctx - ) - .await - } + } => execute_list_orders(kind, currency, status, ctx).await, Commands::NewOrder { kind, fiat_code, @@ -552,86 +540,19 @@ impl Commands { // DM retrieval commands Commands::GetDm { since, from_user } => { - execute_get_dm( - Some(since), - ctx.trade_index, - ctx.mostro_pubkey, - &ctx.mostro_keys, - &ctx.client, - false, - from_user, - &ctx.pool, - ) - .await - } - Commands::GetDmUser { since } => { - execute_get_dm_user(since, &ctx.client, &ctx.mostro_pubkey, &ctx.pool).await + execute_get_dm(Some(since), false, from_user, ctx).await } + Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await, Commands::GetAdminDm { since, from_user } => { - execute_get_dm( - Some(since), - ctx.trade_index, - ctx.mostro_pubkey, - &ctx.mostro_keys, - &ctx.client, - true, - from_user, - &ctx.pool, - ) - .await + execute_get_dm(Some(since), true, from_user, ctx).await } // Admin commands - Commands::AdmListDisputes {} => { - execute_list_disputes( - ctx.mostro_pubkey, - &ctx.mostro_keys, - ctx.trade_index, - &ctx.pool, - &ctx.client, - ) - .await - } - Commands::AdmAddSolver { npubkey } => { - execute_admin_add_solver( - npubkey, - &ctx.mostro_keys, - &ctx.trade_keys, - ctx.mostro_pubkey, - &ctx.client, - ) - .await - } - Commands::AdmSettle { order_id } => { - execute_admin_settle_dispute( - order_id, - &ctx.mostro_keys, - &ctx.trade_keys, - ctx.mostro_pubkey, - &ctx.client, - ) - .await - } - Commands::AdmCancel { order_id } => { - execute_admin_cancel_dispute( - order_id, - &ctx.mostro_keys, - &ctx.trade_keys, - ctx.mostro_pubkey, - &ctx.client, - ) - .await - } - Commands::AdmTakeDispute { dispute_id } => { - execute_take_dispute( - dispute_id, - &ctx.mostro_keys, - &ctx.trade_keys, - ctx.mostro_pubkey, - &ctx.client, - ) - .await - } + Commands::AdmListDisputes {} => execute_list_disputes(ctx).await, + Commands::AdmAddSolver { npubkey } => execute_admin_add_solver(npubkey, ctx).await, + Commands::AdmSettle { order_id } => execute_admin_settle_dispute(order_id, ctx).await, + Commands::AdmCancel { order_id } => execute_admin_cancel_dispute(order_id, ctx).await, + Commands::AdmTakeDispute { dispute_id } => execute_take_dispute(dispute_id, ctx).await, // Simple commands Commands::Restore {} => { diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index da00a69..139ca2d 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -58,17 +58,25 @@ pub async fn execute_add_invoice( .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - // Send DM - send_dm( - client, - Some(identity_keys), - &trade_keys, - &mostro_key, - message_json, - None, - false, - ) - .await?; + // Clone the keys and client for the async call + let identity_keys = identity_keys.clone(); + let trade_keys_clone = trade_keys.clone(); + let client_clone = client.clone(); + + // Spawn a new task to send the DM + // This is so we can wait for the gift wrap event in the main thread + tokio::spawn(async move { + let _ = send_dm( + &client_clone, + Some(&identity_keys), + &trade_keys_clone, + &mostro_key, + message_json, + None, + false, + ) + .await; + }); // Wait for the DM to be sent from mostro and update the order wait_for_dm(client, &trade_keys, request_id, None, Some(order), pool).await?; diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index 671525f..d5edf38 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -1,17 +1,17 @@ use anyhow::Result; use mostro_core::prelude::Message; -use nostr_sdk::prelude::*; -use sqlx::SqlitePool; use crate::{ - cli::Context, parser::dms::print_direct_messages, util::{fetch_events_list, Event, ListKind} + cli::Context, + parser::dms::print_direct_messages, + util::{fetch_events_list, Event, ListKind}, }; pub async fn execute_get_dm( since: Option<&i64>, admin: bool, from_user: &bool, - ctx :&Context + ctx: &Context, ) -> Result<()> { // Get the list kind let list_kind = match (admin, from_user) { @@ -22,17 +22,7 @@ pub async fn execute_get_dm( }; // Fetch the requested events - let all_fetched_events = { - fetch_events_list( - list_kind, - None, - None, - None, - &ctx, - since - ) - .await? - }; + let all_fetched_events = { fetch_events_list(list_kind, None, None, None, ctx, since).await? }; // Extract (Message, u64) tuples from Event::MessageTuple variants let mut dm_events: Vec<(Message, u64)> = Vec::new(); diff --git a/src/cli/get_dm_user.rs b/src/cli/get_dm_user.rs index 6c90936..8577065 100644 --- a/src/cli/get_dm_user.rs +++ b/src/cli/get_dm_user.rs @@ -1,23 +1,17 @@ +use crate::cli::Context; use crate::{db::Order, util::get_direct_messages_from_trade_keys}; use anyhow::Result; use comfy_table::modifiers::UTF8_ROUND_CORNERS; use comfy_table::presets::UTF8_FULL; use comfy_table::Table; use mostro_core::prelude::*; -use nostr_sdk::prelude::*; -use sqlx::SqlitePool; -pub async fn execute_get_dm_user( - since: &i64, - client: &Client, - mostro_pubkey: &PublicKey, - pool: &SqlitePool, -) -> Result<()> { +pub async fn execute_get_dm_user(since: &i64, ctx: &Context) -> Result<()> { // Get all trade keys from orders - let mut trade_keys_hex = Order::get_all_trade_keys(pool).await?; + let mut trade_keys_hex = Order::get_all_trade_keys(&ctx.pool).await?; // Include admin pubkey so we also fetch messages sent TO admin - let admin_pubkey_hex = mostro_pubkey.to_hex(); + let admin_pubkey_hex = ctx.mostro_pubkey.to_hex(); if !trade_keys_hex.iter().any(|k| k == &admin_pubkey_hex) { trade_keys_hex.push(admin_pubkey_hex); } @@ -35,8 +29,13 @@ pub async fn execute_get_dm_user( trade_keys_hex.len() ); - let direct_messages = - get_direct_messages_from_trade_keys(client, trade_keys_hex, *since, mostro_pubkey).await?; + let direct_messages = get_direct_messages_from_trade_keys( + &ctx.client, + trade_keys_hex, + *since, + &ctx.mostro_pubkey, + ) + .await?; if direct_messages.is_empty() { println!("You don't have any direct messages in your trade keys"); diff --git a/src/cli/list_disputes.rs b/src/cli/list_disputes.rs index 7ae1fd6..feff25b 100644 --- a/src/cli/list_disputes.rs +++ b/src/cli/list_disputes.rs @@ -1,36 +1,19 @@ use anyhow::Result; -use nostr_sdk::prelude::*; -use sqlx::SqlitePool; +use crate::cli::Context; use crate::parser::disputes::print_disputes_table; use crate::util::{fetch_events_list, ListKind}; -pub async fn execute_list_disputes( - mostro_pubkey: PublicKey, - mostro_keys: &Keys, - trade_index: i64, - pool: &SqlitePool, - client: &Client, -) -> Result<()> { +pub async fn execute_list_disputes(ctx: &Context) -> Result<()> { + // Print mostro pubkey println!( "Requesting disputes from mostro pubId - {}", - mostro_pubkey.clone() + &ctx.mostro_pubkey ); // Get orders from relays - let table_of_disputes = fetch_events_list( - ListKind::Disputes, - None, - None, - None, - &mostro_pubkey, - mostro_keys, - trade_index, - None, - pool, - client, - ) - .await?; + let table_of_disputes = + fetch_events_list(ListKind::Disputes, None, None, None, ctx, None).await?; let table = print_disputes_table(table_of_disputes)?; println!("{table}"); diff --git a/src/cli/list_orders.rs b/src/cli/list_orders.rs index caab880..56891c3 100644 --- a/src/cli/list_orders.rs +++ b/src/cli/list_orders.rs @@ -1,11 +1,9 @@ +use crate::cli::Context; use crate::parser::orders::print_orders_table; use crate::util::{fetch_events_list, ListKind}; use anyhow::Result; use mostro_core::prelude::*; -use nostr_sdk::prelude::*; -use sqlx::SqlitePool; use std::str::FromStr; -use crate::cli::Context; #[allow(clippy::too_many_arguments)] pub async fn execute_list_orders( @@ -52,7 +50,10 @@ pub async fn execute_list_orders( } } - println!("Requesting orders from mostro pubId - {}", &ctx.mostro_pubkey); + println!( + "Requesting orders from mostro pubId - {}", + &ctx.mostro_pubkey + ); // Get orders from relays let table_of_orders = fetch_events_list( @@ -60,8 +61,8 @@ pub async fn execute_list_orders( status_checked, upper_currency, kind_checked, - &ctx, - since + ctx, + None, ) .await?; let table = print_orders_table(table_of_orders)?; diff --git a/src/cli/send_msg.rs b/src/cli/send_msg.rs index 0672667..147c08c 100644 --- a/src/cli/send_msg.rs +++ b/src/cli/send_msg.rs @@ -5,7 +5,6 @@ use crate::util::{send_dm, wait_for_dm}; use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; -use sqlx::SqlitePool; use std::process; use uuid::Uuid; @@ -95,9 +94,16 @@ pub async fn execute_send_msg( .map_err(|e| anyhow::anyhow!("Failed to send DM: {e}"))?; // Wait for the DM to be sent from mostro - wait_for_dm(&ctx.client, &trade_keys, request_id, None, Some(order), &ctx.pool) - .await - .map_err(|e| anyhow::anyhow!("Failed to wait for DM: {e}"))?; + wait_for_dm( + &ctx.client, + &trade_keys, + request_id, + None, + Some(order), + &ctx.pool, + ) + .await + .map_err(|e| anyhow::anyhow!("Failed to wait for DM: {e}"))?; } } diff --git a/src/cli/take_dispute.rs b/src/cli/take_dispute.rs index 6cdbe0e..9da4a8a 100644 --- a/src/cli/take_dispute.rs +++ b/src/cli/take_dispute.rs @@ -1,21 +1,13 @@ use anyhow::Result; use mostro_core::prelude::*; -use nostr_sdk::prelude::*; use uuid::Uuid; -use crate::util::send_dm; +use crate::{cli::Context, util::send_dm}; -pub async fn execute_admin_add_solver( - npubkey: &str, - identity_keys: &Keys, - trade_keys: &Keys, - mostro_key: PublicKey, - client: &Client, -) -> Result<()> { +pub async fn execute_admin_add_solver(npubkey: &str, ctx: &Context) -> Result<()> { println!( "Request of add solver with pubkey {} from mostro pubId {}", - npubkey, - mostro_key.clone() + npubkey, &ctx.mostro_pubkey ); // Create takebuy message let take_dispute_message = Message::new_dispute( @@ -29,10 +21,10 @@ pub async fn execute_admin_add_solver( .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; send_dm( - client, - Some(identity_keys), - trade_keys, - &mostro_key, + &ctx.client, + Some(&ctx.identity_keys), + &ctx.trade_keys, + &ctx.mostro_pubkey, take_dispute_message, None, false, @@ -42,17 +34,11 @@ pub async fn execute_admin_add_solver( Ok(()) } -pub async fn execute_admin_cancel_dispute( - dispute_id: &Uuid, - identity_keys: &Keys, - trade_keys: &Keys, - mostro_key: PublicKey, - client: &Client, -) -> Result<()> { +pub async fn execute_admin_cancel_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<()> { println!( "Request of cancel dispute {} from mostro pubId {}", dispute_id, - mostro_key.clone() + ctx.mostro_pubkey.clone() ); // Create takebuy message let take_dispute_message = @@ -60,13 +46,16 @@ pub async fn execute_admin_cancel_dispute( .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - println!("identity_keys: {:?}", identity_keys.public_key.to_string()); + println!( + "identity_keys: {:?}", + ctx.identity_keys.public_key.to_string() + ); send_dm( - client, - Some(identity_keys), - trade_keys, - &mostro_key, + &ctx.client, + Some(&ctx.identity_keys), + &ctx.trade_keys, + &ctx.mostro_pubkey, take_dispute_message, None, false, @@ -76,17 +65,11 @@ pub async fn execute_admin_cancel_dispute( Ok(()) } -pub async fn execute_admin_settle_dispute( - dispute_id: &Uuid, - identity_keys: &Keys, - trade_keys: &Keys, - mostro_key: PublicKey, - client: &Client, -) -> Result<()> { +pub async fn execute_admin_settle_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<()> { println!( "Request of take dispute {} from mostro pubId {}", dispute_id, - mostro_key.clone() + ctx.mostro_pubkey.clone() ); // Create takebuy message let take_dispute_message = @@ -94,13 +77,16 @@ pub async fn execute_admin_settle_dispute( .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - println!("identity_keys: {:?}", identity_keys.public_key.to_string()); + println!( + "identity_keys: {:?}", + ctx.identity_keys.public_key.to_string() + ); send_dm( - client, - Some(identity_keys), - trade_keys, - &mostro_key, + &ctx.client, + Some(&ctx.identity_keys), + &ctx.trade_keys, + &ctx.mostro_pubkey, take_dispute_message, None, false, @@ -110,17 +96,11 @@ pub async fn execute_admin_settle_dispute( Ok(()) } -pub async fn execute_take_dispute( - dispute_id: &Uuid, - identity_keys: &Keys, - trade_keys: &Keys, - mostro_key: PublicKey, - client: &Client, -) -> Result<()> { +pub async fn execute_take_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<()> { println!( "Request of take dispute {} from mostro pubId {}", dispute_id, - mostro_key.clone() + ctx.mostro_pubkey.clone() ); // Create takebuy message let take_dispute_message = Message::new_dispute( @@ -133,13 +113,16 @@ pub async fn execute_take_dispute( .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - println!("identity_keys: {:?}", identity_keys.public_key.to_string()); + println!( + "identity_keys: {:?}", + ctx.identity_keys.public_key.to_string() + ); send_dm( - client, - Some(identity_keys), - trade_keys, - &mostro_key, + &ctx.client, + Some(&ctx.identity_keys), + &ctx.trade_keys, + &ctx.mostro_pubkey, take_dispute_message, None, false, diff --git a/src/util.rs b/src/util.rs index cab301d..bf0bf85 100644 --- a/src/util.rs +++ b/src/util.rs @@ -94,7 +94,7 @@ pub async fn save_order( order: SmallOrder, trade_keys: &Keys, request_id: u64, - trade_index: i64, + trade_index: Option, pool: &SqlitePool, ) -> Result<()> { if let Ok(order) = Order::new(pool, order, trade_keys, Some(request_id as i64)).await { @@ -103,6 +103,15 @@ pub async fn save_order( } else { println!("Warning: The newly created order has no ID."); } + // Get trade index - we must have it + let trade_index = if let Some(trade_index) = trade_index { + trade_index + } else { + return Err(anyhow::anyhow!( + "No trade index found for new order, this should never happen" + )); + }; + // Update last trade index to be used in next trade match User::get(pool).await { Ok(mut user) => { @@ -127,15 +136,8 @@ pub async fn wait_for_dm( pool: &SqlitePool, ) -> anyhow::Result<()> { let mut notifications = client.notifications(); - // Get trade index - let trade_index = if let Some(trade_index) = trade_index { - trade_index - } else { - println!("Trade index not used for this action"); - 0 - }; - match tokio::time::timeout(Duration::from_secs(10), async move { + match tokio::time::timeout(Duration::from_secs(15), async move { while let Ok(notification) = notifications.recv().await { if let RelayPoolNotification::Event { event, .. } = notification { if event.kind == nostr_sdk::Kind::GiftWrap { @@ -154,6 +156,7 @@ pub async fn wait_for_dm( } }; let message = message.get_inner_message_kind(); + println!("Received message: {:?}", message); if message.request_id == Some(request_id) { match message.action { Action::NewOrder => { @@ -568,7 +571,8 @@ pub async fn fetch_events_list( match list_kind { ListKind::Orders => { let filters = create_filter(list_kind, ctx.mostro_pubkey, None); - let fetched_events = ctx.client + let fetched_events = ctx + .client .fetch_events(filters, Duration::from_secs(15)) .await?; let orders = parse_orders_events(fetched_events, currency, status, kind); @@ -576,7 +580,8 @@ pub async fn fetch_events_list( } ListKind::DirectMessagesAdmin => { let filters = create_filter(list_kind, ctx.mostro_pubkey, None); - let fetched_events = ctx.client + let fetched_events = ctx + .client .fetch_events(filters, Duration::from_secs(15)) .await?; let direct_messages_mostro = parse_dm_events(fetched_events, &ctx.context_keys).await; @@ -594,8 +599,10 @@ pub async fn fetch_events_list( trade_key.public_key(), None, ); - let fetched_user_messages = - ctx.client.fetch_events(filter, Duration::from_secs(15)).await?; + let fetched_user_messages = ctx + .client + .fetch_events(filter, Duration::from_secs(15)) + .await?; let direct_messages_for_trade_key = parse_dm_events(fetched_user_messages, &trade_key).await; direct_messages.extend( @@ -615,8 +622,10 @@ pub async fn fetch_events_list( let trade_key = User::get_trade_keys(&ctx.pool, index).await?; let filter = create_filter(ListKind::DirectMessagesUser, trade_key.public_key(), None); - let fetched_user_messages = - ctx.client.fetch_events(filter, Duration::from_secs(15)).await?; + let fetched_user_messages = ctx + .client + .fetch_events(filter, Duration::from_secs(15)) + .await?; let direct_messages_for_trade_key = parse_dm_events(fetched_user_messages, &trade_key).await; direct_messages.extend( @@ -631,8 +640,9 @@ pub async fn fetch_events_list( .collect()) } ListKind::Disputes => { - let filters = create_filter(list_kind, *mostro_pubkey, None); - let fetched_events = client + let filters = create_filter(list_kind, ctx.mostro_pubkey, None); + let fetched_events = ctx + .client .fetch_events(filters, Duration::from_secs(15)) .await?; let disputes = parse_dispute_events(fetched_events); @@ -660,16 +670,6 @@ pub fn get_mcli_path() -> String { mcli_path } -pub async fn run_simple_order_msg( - command: Commands, - order_id: &Uuid, - ctx: &Context, -) -> Result<()> { - execute_send_msg( - command, - Some(*order_id), - &ctx, - None, - ) - .await +pub async fn run_simple_order_msg(command: Commands, order_id: &Uuid, ctx: &Context) -> Result<()> { + execute_send_msg(command, Some(*order_id), ctx, None).await } From aaba2db6178f583feddaf45ff36a574b7bded5c2 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Tue, 16 Sep 2025 16:56:39 +0200 Subject: [PATCH 16/19] feat: context used in all functions, not compiling for another optimization to complete --- src/cli.rs | 59 ++++-------------------------------------- src/cli/add_invoice.rs | 35 +++++++++++++------------ src/cli/new_order.rs | 46 +++++++++++++++++--------------- src/cli/rate_user.rs | 38 +++++++++++---------------- src/cli/take_order.rs | 50 ++++++++++++++++------------------- src/db.rs | 7 ----- 6 files changed, 85 insertions(+), 150 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 1db0edf..c123706 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -471,13 +471,8 @@ impl Commands { payment_method, premium, invoice, - &ctx.identity_keys, - &ctx.trade_keys, - ctx.trade_index, - ctx.mostro_pubkey, - &ctx.client, + ctx, expiration_days, - &ctx.pool, ) .await } @@ -485,58 +480,14 @@ impl Commands { order_id, invoice, amount, - } => { - execute_take_order( - order_id, - Action::TakeSell, - invoice, - *amount, - &ctx.identity_keys, - &ctx.trade_keys, - ctx.trade_index, - ctx.mostro_pubkey, - &ctx.client, - &ctx.pool, - ) - .await - } + } => execute_take_order(order_id, Action::TakeSell, invoice, *amount, ctx).await, Commands::TakeBuy { order_id, amount } => { - execute_take_order( - order_id, - Action::TakeBuy, - &None, - *amount, - &ctx.identity_keys, - &ctx.trade_keys, - ctx.trade_index, - ctx.mostro_pubkey, - &ctx.client, - &ctx.pool, - ) - .await + execute_take_order(order_id, Action::TakeBuy, &None, *amount, ctx).await } Commands::AddInvoice { order_id, invoice } => { - execute_add_invoice( - order_id, - invoice, - &ctx.identity_keys, - ctx.mostro_pubkey, - &ctx.client, - &ctx.pool, - ) - .await - } - Commands::Rate { order_id, rating } => { - execute_rate_user( - order_id, - rating, - &ctx.identity_keys, - ctx.mostro_pubkey, - &ctx.client, - &ctx.pool, - ) - .await + execute_add_invoice(order_id, invoice, ctx).await } + Commands::Rate { order_id, rating } => execute_rate_user(order_id, rating, ctx).await, // DM retrieval commands Commands::GetDm { since, from_user } => { diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index 139ca2d..b200782 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -1,23 +1,15 @@ use crate::util::{send_dm, wait_for_dm}; -use crate::{db::Order, lightning::is_valid_invoice}; +use crate::{cli::Context, db::Order, lightning::is_valid_invoice}; use anyhow::Result; use lnurl::lightning_address::LightningAddress; use mostro_core::prelude::*; use nostr_sdk::prelude::*; -use sqlx::SqlitePool; use std::str::FromStr; use uuid::Uuid; -pub async fn execute_add_invoice( - order_id: &Uuid, - invoice: &str, - identity_keys: &Keys, - mostro_key: PublicKey, - client: &Client, - pool: &SqlitePool, -) -> Result<()> { +pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) -> Result<()> { println!("Adding invoice to order {}", order_id); - let order = Order::get_by_id(pool, &order_id.to_string()).await?; + let order = Order::get_by_id(&ctx.pool, &order_id.to_string()).await?; let trade_keys = order .trade_keys .clone() @@ -26,7 +18,7 @@ pub async fn execute_add_invoice( println!( "Sending a lightning invoice {} to mostro pubId {}", - order_id, mostro_key + order_id, ctx.mostro_pubkey ); // Check invoice string let ln_addr = LightningAddress::from_str(invoice); @@ -59,18 +51,19 @@ pub async fn execute_add_invoice( .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; // Clone the keys and client for the async call - let identity_keys = identity_keys.clone(); + let identity_keys_clone = ctx.identity_keys.clone(); let trade_keys_clone = trade_keys.clone(); - let client_clone = client.clone(); + let client_clone = ctx.client.clone(); + let mostro_pubkey_clone = ctx.mostro_pubkey; // Spawn a new task to send the DM // This is so we can wait for the gift wrap event in the main thread tokio::spawn(async move { let _ = send_dm( &client_clone, - Some(&identity_keys), + Some(&identity_keys_clone), &trade_keys_clone, - &mostro_key, + &mostro_pubkey_clone, message_json, None, false, @@ -79,6 +72,14 @@ pub async fn execute_add_invoice( }); // Wait for the DM to be sent from mostro and update the order - wait_for_dm(client, &trade_keys, request_id, None, Some(order), pool).await?; + wait_for_dm( + &ctx.client, + &ctx.trade_keys, + request_id, + None, + Some(order), + &ctx.pool, + ) + .await?; Ok(()) } diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index 51fe75a..244a6ab 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -1,9 +1,9 @@ +use crate::cli::Context; use crate::parser::orders::print_order_preview; use crate::util::{send_dm, uppercase_first, wait_for_dm}; use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; -use sqlx::SqlitePool; use std::collections::HashMap; use std::io::{stdin, stdout, BufRead, Write}; use std::process; @@ -12,6 +12,12 @@ use uuid::Uuid; pub type FiatNames = HashMap; +fn set_order_values() -> Result { + let mut new_order= SmallOrder::default(); + + ; +} + #[allow(clippy::too_many_arguments)] pub async fn execute_new_order( kind: &str, @@ -21,13 +27,8 @@ pub async fn execute_new_order( payment_method: &str, premium: &i64, invoice: &Option, - identity_keys: &Keys, - trade_keys: &Keys, - trade_index: i64, - mostro_key: PublicKey, - client: &Client, + ctx: &Context, expiration_days: &i64, - pool: &SqlitePool, ) -> Result<()> { // Uppercase currency let fiat_code = fiat_code.to_uppercase(); @@ -67,6 +68,9 @@ pub async fn execute_new_order( } else { (fiat_amount.0, None, None) }; + + let small_order = set_order_values(kind, fiat_code, amt, payment_method, premium, invoice, expires_at)?; + let small_order = SmallOrder::new( None, Some(kind_checked), @@ -116,7 +120,7 @@ pub async fn execute_new_order( let message = Message::new_order( None, Some(request_id), - Some(trade_index), + Some(ctx.trade_index), Action::NewOrder, Some(order_content), ); @@ -124,7 +128,7 @@ pub async fn execute_new_order( // Send dm to receiver pubkey println!( "SENDING DM with trade keys: {:?}", - trade_keys.public_key().to_hex() + ctx.trade_keys.public_key().to_hex() ); // Serialize the message @@ -133,29 +137,29 @@ pub async fn execute_new_order( .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; // Clone the keys and client for the async call - let identity_keys = identity_keys.clone(); - let trade_keys_clone = trade_keys.clone(); - // let mostro_key = mostro_key.clone(); - let client_clone = client.clone(); + let identity_keys_clone = ctx.identity_keys.clone(); + let trade_keys_clone = ctx.trade_keys.clone(); + let client_clone = ctx.client.clone(); + let mostro_pubkey_clone = ctx.mostro_pubkey; // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 let subscription = Filter::new() - .pubkey(trade_keys.public_key()) + .pubkey(ctx.trade_keys.public_key()) .kind(nostr_sdk::Kind::GiftWrap) .limit(0); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); - client.subscribe(subscription, Some(opts)).await?; + ctx.client.subscribe(subscription, Some(opts)).await?; // Spawn a new task to send the DM // This is so we can wait for the gift wrap event in the main thread tokio::spawn(async move { let _ = send_dm( &client_clone, - Some(&identity_keys.clone()), + Some(&identity_keys_clone), &trade_keys_clone, - &mostro_key, + &mostro_pubkey_clone, message_json, None, false, @@ -165,12 +169,12 @@ pub async fn execute_new_order( // Wait for the DM to be sent from mostro wait_for_dm( - client, - trade_keys, + &ctx.client, + &ctx.trade_keys, request_id, - Some(trade_index), + Some(ctx.trade_index), None, - pool, + &ctx.pool, ) .await?; diff --git a/src/cli/rate_user.rs b/src/cli/rate_user.rs index e3de6a2..eb83452 100644 --- a/src/cli/rate_user.rs +++ b/src/cli/rate_user.rs @@ -1,12 +1,11 @@ use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; -use sqlx::SqlitePool; use uuid::Uuid; const RATING_BOUNDARIES: [u8; 5] = [1, 2, 3, 4, 5]; -use crate::{db::Order, util::send_dm}; +use crate::{cli::Context, db::Order, util::send_dm}; // Get the user rate fn get_user_rate(rating: &u8) -> Result { @@ -17,29 +16,22 @@ fn get_user_rate(rating: &u8) -> Result { } } -pub async fn execute_rate_user( - order_id: &Uuid, - rating: &u8, - identity_keys: &Keys, - mostro_key: PublicKey, - client: &Client, - pool: &SqlitePool, -) -> Result<()> { +pub async fn execute_rate_user(order_id: &Uuid, rating: &u8, ctx: &Context) -> Result<()> { // Check boundaries let rating_content = get_user_rate(rating)?; // Get the trade keys - let trade_keys = if let Ok(order_to_vote) = Order::get_by_id(pool, &order_id.to_string()).await - { - match order_to_vote.trade_keys.as_ref() { - Some(trade_keys) => Keys::parse(trade_keys)?, - None => { - return Err(anyhow::anyhow!("No trade_keys found for this order")); + let trade_keys = + if let Ok(order_to_vote) = Order::get_by_id(&ctx.pool, &order_id.to_string()).await { + match order_to_vote.trade_keys.as_ref() { + Some(trade_keys) => Keys::parse(trade_keys)?, + None => { + return Err(anyhow::anyhow!("No trade_keys found for this order")); + } } - } - } else { - return Err(anyhow::anyhow!("order {} not found", order_id)); - }; + } else { + return Err(anyhow::anyhow!("order {} not found", order_id)); + }; // Create rating message of counterpart let rate_message = Message::new_order( @@ -53,10 +45,10 @@ pub async fn execute_rate_user( .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; send_dm( - client, - Some(identity_keys), + &ctx.client, + Some(&ctx.identity_keys), &trade_keys, - &mostro_key, + &ctx.mostro_pubkey, rate_message, None, false, diff --git a/src/cli/take_order.rs b/src/cli/take_order.rs index 482e01b..62fe4f5 100644 --- a/src/cli/take_order.rs +++ b/src/cli/take_order.rs @@ -2,12 +2,12 @@ use anyhow::Result; use lnurl::lightning_address::LightningAddress; use mostro_core::prelude::*; use nostr_sdk::prelude::*; -use sqlx::SqlitePool; use std::str::FromStr; use uuid::Uuid; +use crate::cli::Context; use crate::lightning::is_valid_invoice; -use crate::util; +use crate::util::{send_dm, wait_for_dm}; /// Create payload based on action type and parameters fn create_take_order_payload( @@ -55,12 +55,7 @@ pub async fn execute_take_order( action: Action, invoice: &Option, amount: Option, - identity_keys: &Keys, - trade_keys: &Keys, - trade_index: i64, - mostro_key: PublicKey, - client: &Client, - pool: &SqlitePool, + ctx: &Context, ) -> Result<()> { let action_name = match action { Action::TakeBuy => "take buy", @@ -70,9 +65,7 @@ pub async fn execute_take_order( println!( "Request of {} order {} from mostro pubId {}", - action_name, - order_id, - mostro_key.clone() + action_name, order_id, ctx.mostro_pubkey ); // Create payload based on action type @@ -84,7 +77,7 @@ pub async fn execute_take_order( let take_order_message = Message::new_order( Some(*order_id), Some(request_id), - Some(trade_index), + Some(ctx.trade_index), action.clone(), payload, ); @@ -92,7 +85,7 @@ pub async fn execute_take_order( // Send dm to receiver pubkey println!( "SENDING DM with trade keys: {:?}", - trade_keys.public_key().to_hex() + ctx.trade_keys.public_key().to_hex() ); let message_json = take_order_message @@ -100,27 +93,28 @@ pub async fn execute_take_order( .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; // Clone the keys and client for the async call - let identity_keys = identity_keys.clone(); - let trade_keys_clone = trade_keys.clone(); - let client_clone = client.clone(); + let identity_keys_clone = ctx.identity_keys.clone(); + let trade_keys_clone = ctx.trade_keys.clone(); + let client_clone = ctx.client.clone(); + let mostro_pubkey_clone = ctx.mostro_pubkey; // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 let subscription = Filter::new() - .pubkey(trade_keys.public_key()) + .pubkey(ctx.trade_keys.public_key()) .kind(nostr_sdk::Kind::GiftWrap) .limit(0); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); - client.subscribe(subscription, Some(opts)).await?; + ctx.client.subscribe(subscription, Some(opts)).await?; // Spawn a new task to send the DM // This is so we can wait for the gift wrap event in the main thread tokio::spawn(async move { - let _ = util::send_dm( + let _ = send_dm( &client_clone, - Some(&identity_keys.clone()), + Some(&identity_keys_clone), &trade_keys_clone, - &mostro_key, + &mostro_pubkey_clone, message_json, None, false, @@ -131,22 +125,22 @@ pub async fn execute_take_order( // For take_sell, add an additional subscription with timestamp filtering if action == Action::TakeSell { let subscription = Filter::new() - .pubkey(trade_keys.public_key()) + .pubkey(ctx.trade_keys.public_key()) .kind(nostr_sdk::Kind::GiftWrap) .since(Timestamp::from(chrono::Utc::now().timestamp() as u64)) .limit(0); - client.subscribe(subscription, None).await?; + ctx.client.subscribe(subscription, None).await?; } // Wait for the DM to be sent from mostro - util::wait_for_dm( - client, - trade_keys, + wait_for_dm( + &ctx.client, + &ctx.trade_keys, request_id, - Some(trade_index), + Some(ctx.trade_index), None, - pool, + &ctx.pool, ) .await?; diff --git a/src/db.rs b/src/db.rs index e5f0a8f..f5d9e04 100644 --- a/src/db.rs +++ b/src/db.rs @@ -476,13 +476,6 @@ impl Order { Ok(order) } - pub async fn get_all(pool: &SqlitePool) -> Result> { - let orders = sqlx::query_as::<_, Order>(r#"SELECT * FROM orders"#) - .fetch_all(pool) - .await?; - Ok(orders) - } - pub async fn get_all_trade_keys(pool: &SqlitePool) -> Result> { let trade_keys: Vec = sqlx::query_scalar::<_, Option>( "SELECT DISTINCT trade_keys FROM orders WHERE trade_keys IS NOT NULL", From 24d47e67170f41adae265cc03ab021bd7a3e60fa Mon Sep 17 00:00:00 2001 From: arkanoider Date: Tue, 16 Sep 2025 22:52:44 +0200 Subject: [PATCH 17/19] refactor: fixed some errors on parsing - now it's testable --- src/cli/add_invoice.rs | 27 +++++++++++++++++++-------- src/cli/new_order.rs | 8 -------- src/cli/send_msg.rs | 4 +--- src/util.rs | 7 ++----- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index b200782..ebf4d61 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -8,16 +8,19 @@ use std::str::FromStr; use uuid::Uuid; pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) -> Result<()> { - println!("Adding invoice to order {}", order_id); let order = Order::get_by_id(&ctx.pool, &order_id.to_string()).await?; let trade_keys = order .trade_keys .clone() .ok_or(anyhow::anyhow!("Missing trade keys"))?; - let trade_keys = Keys::parse(&trade_keys)?; + let order_trade_keys = Keys::parse(&trade_keys)?; + println!( + "Order trade keys: {:?}", + order_trade_keys.public_key().to_hex() + ); println!( - "Sending a lightning invoice {} to mostro pubId {}", + "Sending a lightning invoice for order {} to mostro pubId {}", order_id, ctx.mostro_pubkey ); // Check invoice string @@ -28,8 +31,7 @@ pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) match is_valid_invoice(invoice) { Ok(i) => Some(Payload::PaymentRequest(None, i.to_string(), None)), Err(e) => { - println!("Invalid invoice: {}", e); - None + return Err(anyhow::anyhow!("Invalid invoice: {}", e)); } } }; @@ -50,11 +52,20 @@ pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; + // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0 + let subscription = Filter::new() + .pubkey(order_trade_keys.clone().public_key()) + .kind(nostr_sdk::Kind::GiftWrap) + .limit(0); + + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1)); + ctx.client.subscribe(subscription, Some(opts)).await?; + // Clone the keys and client for the async call let identity_keys_clone = ctx.identity_keys.clone(); - let trade_keys_clone = trade_keys.clone(); let client_clone = ctx.client.clone(); let mostro_pubkey_clone = ctx.mostro_pubkey; + let order_trade_keys_clone = order_trade_keys.clone(); // Spawn a new task to send the DM // This is so we can wait for the gift wrap event in the main thread @@ -62,7 +73,7 @@ pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) let _ = send_dm( &client_clone, Some(&identity_keys_clone), - &trade_keys_clone, + &order_trade_keys, &mostro_pubkey_clone, message_json, None, @@ -74,7 +85,7 @@ pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) // Wait for the DM to be sent from mostro and update the order wait_for_dm( &ctx.client, - &ctx.trade_keys, + &order_trade_keys_clone, request_id, None, Some(order), diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index 244a6ab..4bf5ed7 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -12,12 +12,6 @@ use uuid::Uuid; pub type FiatNames = HashMap; -fn set_order_values() -> Result { - let mut new_order= SmallOrder::default(); - - ; -} - #[allow(clippy::too_many_arguments)] pub async fn execute_new_order( kind: &str, @@ -69,8 +63,6 @@ pub async fn execute_new_order( (fiat_amount.0, None, None) }; - let small_order = set_order_values(kind, fiat_code, amt, payment_method, premium, invoice, expires_at)?; - let small_order = SmallOrder::new( None, Some(kind_checked), diff --git a/src/cli/send_msg.rs b/src/cli/send_msg.rs index 147c08c..6701b59 100644 --- a/src/cli/send_msg.rs +++ b/src/cli/send_msg.rs @@ -5,7 +5,6 @@ use crate::util::{send_dm, wait_for_dm}; use anyhow::Result; use mostro_core::prelude::*; use nostr_sdk::prelude::*; -use std::process; use uuid::Uuid; pub async fn execute_send_msg( @@ -24,8 +23,7 @@ pub async fn execute_send_msg( Commands::AdmSettle { .. } => Action::AdminSettle, Commands::AdmAddSolver { .. } => Action::AdminAddSolver, _ => { - eprintln!("Not a valid command!"); - process::exit(0); + return Err(anyhow::anyhow!("Invalid command for send msg")); } }; diff --git a/src/util.rs b/src/util.rs index bf0bf85..2ccfd5e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -156,7 +156,6 @@ pub async fn wait_for_dm( } }; let message = message.get_inner_message_kind(); - println!("Received message: {:?}", message); if message.request_id == Some(request_id) { match message.action { Action::NewOrder => { @@ -180,6 +179,7 @@ pub async fn wait_for_dm( Ok(_) => println!("Order status updated"), Err(e) => println!("Failed to update order status: {}", e), } + return Ok(()); } } // this is the case where the buyer adds an invoice to a takesell order @@ -260,10 +260,7 @@ pub async fn wait_for_dm( } } } - _ => { - println!("Unknown action: {:?}", message.action); - return Err(()); - } + _ => {} } } } From 7081a339f4eb38d101a662be32ec442a80d44155 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Wed, 17 Sep 2025 10:20:51 +0200 Subject: [PATCH 18/19] refactor: removed all unwraps and improved some part of filter creation --- src/fiat.rs | 8 ++- src/util.rs | 141 ++++++++++++++++++++++++---------------------------- 2 files changed, 69 insertions(+), 80 deletions(-) diff --git a/src/fiat.rs b/src/fiat.rs index a7d01be..238907b 100644 --- a/src/fiat.rs +++ b/src/fiat.rs @@ -1453,14 +1453,12 @@ pub fn load_fiat_values() -> FiatList { } }"#; - let fiat_json: FiatNames = serde_json::from_str(fiat_names).unwrap(); - + // Parse fiat names + let fiat_json = serde_json::from_str(fiat_names).map_err(|e| anyhow::anyhow!("Failed to parse fiat names: {}", e))?; let mut fiatlist = FiatList::new(); - for elem in fiat_json.iter() { fiatlist.push((elem.0.to_string(), elem.1.name.clone())); - } - + //Return list fiatlist.sort_by(|a, b| a.0.cmp(&b.0)); diff --git a/src/util.rs b/src/util.rs index 2ccfd5e..e6b5477 100644 --- a/src/util.rs +++ b/src/util.rs @@ -15,6 +15,9 @@ use std::time::Duration; use std::{fs, path::Path}; use uuid::Uuid; +const FAKE_SINCE: i64 = 2880; +const FETCH_EVENTS_TIMEOUT: Duration = Duration::from_secs(15); + #[derive(Clone, Debug)] pub enum Event { SmallOrder(SmallOrder), @@ -137,7 +140,7 @@ pub async fn wait_for_dm( ) -> anyhow::Result<()> { let mut notifications = client.notifications(); - match tokio::time::timeout(Duration::from_secs(15), async move { + match tokio::time::timeout(FETCH_EVENTS_TIMEOUT, async move { while let Ok(notification) = notifications.recv().await { if let RelayPoolNotification::Event { event, .. } = notification { if event.kind == nostr_sdk::Kind::GiftWrap { @@ -374,11 +377,14 @@ pub async fn send_dm( expiration: Option, to_user: bool, ) -> Result<()> { - let pow: u8 = var("POW").unwrap_or('0'.to_string()).parse().unwrap(); + let pow: u8 = var("POW") + .unwrap_or('0'.to_string()) + .parse() + .map_err(|e| anyhow::anyhow!("Failed to parse POW: {}", e))?; let private = var("SECRET") .unwrap_or("false".to_string()) .parse::() - .unwrap(); + .map_err(|e| anyhow::anyhow!("Failed to parse SECRET: {}", e))?; let message_type = determine_message_type(to_user, private); @@ -444,17 +450,10 @@ pub async fn get_direct_messages_from_trade_keys( return Ok(Vec::new()); } - let fake_since = 2880; - let fake_since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(fake_since)) - .unwrap() - .timestamp() as u64; - let _fake_timestamp = Timestamp::from(fake_since_time); - - let _since_time = chrono::Utc::now() + let since_time = chrono::Utc::now() .checked_sub_signed(chrono::Duration::minutes(since)) - .unwrap() - .timestamp() as u64; + .ok_or(anyhow::anyhow!("Failed to get since time"))? + .timestamp(); // Get the triple of message, timestamp and public key let mut all_messages: Vec<(Message, u64, PublicKey)> = Vec::new(); @@ -464,8 +463,9 @@ pub async fn get_direct_messages_from_trade_keys( for trade_key_hex in trade_keys_hex { if let Ok(public_key) = PublicKey::from_hex(&trade_key_hex) { // Create filter for fetching direct messages - let filter = create_filter(ListKind::DirectMessagesUser, public_key, None); - let events = client.fetch_events(filter, Duration::from_secs(15)).await?; + let filter = + create_filter(ListKind::DirectMessagesUser, public_key, Some(&since_time))?; + let events = client.fetch_events(filter, FETCH_EVENTS_TIMEOUT).await?; // Parse events without keys since we only have the public key // We'll need to handle this differently - let's just collect the events for now for event in events { @@ -478,67 +478,58 @@ pub async fn get_direct_messages_from_trade_keys( } } } - Ok(all_messages) } -pub fn create_filter(list_kind: ListKind, pubkey: PublicKey, since: Option<&u64>) -> Filter { +/// Create a fake timestamp to thwart time-analysis attacks +fn create_fake_timestamp() -> Result { + let fake_since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(FAKE_SINCE)) + .ok_or(anyhow::anyhow!("Failed to get fake since time"))? + .timestamp() as u64; + Ok(Timestamp::from(fake_since_time)) +} + +// Create a filter for fetching events in the last 7 days +fn create_seven_days_filter(letter: Alphabet, value: String, pubkey: PublicKey) -> Result { + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::days(7)) + .ok_or(anyhow::anyhow!("Failed to get since days ago"))? + .timestamp() as u64; + + let timestamp = Timestamp::from(since_time); + + Ok(Filter::new() + .author(pubkey) + .limit(50) + .since(timestamp) + .custom_tag(SingleLetterTag::lowercase(letter), value) + .kind(nostr_sdk::Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND))) +} + +// Create a filter for fetching events +pub fn create_filter( + list_kind: ListKind, + pubkey: PublicKey, + since: Option<&i64>, +) -> Result { match list_kind { - ListKind::Orders => { - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::days(7)) - .unwrap() - .timestamp() as u64; - - let timestamp = Timestamp::from(since_time); - - // Create filter for fetching orders - Filter::new() - .author(pubkey) - .limit(50) - .since(timestamp) - .custom_tag(SingleLetterTag::lowercase(Alphabet::Z), "order".to_string()) - .kind(nostr_sdk::Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND)) - } - ListKind::Disputes => { - let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::days(7)) - .unwrap() - .timestamp() as u64; - - let timestamp = Timestamp::from(since_time); - - // Create filter for fetching orders - Filter::new() - .author(pubkey) - .limit(50) - .since(timestamp) - .custom_tag( - SingleLetterTag::lowercase(Alphabet::Z), - "dispute".to_string(), - ) - .kind(nostr_sdk::Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND)) - } + ListKind::Orders => create_seven_days_filter(Alphabet::Z, "order".to_string(), pubkey), + ListKind::Disputes => create_seven_days_filter(Alphabet::Z, "dispute".to_string(), pubkey), ListKind::DirectMessagesAdmin | ListKind::DirectMessagesUser => { // We use a fake timestamp to thwart time-analysis attacks - let fake_since = 2880; - let fake_since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(fake_since)) - .unwrap() - .timestamp() as u64; - - let fake_timestamp = Timestamp::from(fake_since_time); + let fake_timestamp = create_fake_timestamp()?; - Filter::new() + Ok(Filter::new() .kind(nostr_sdk::Kind::GiftWrap) .pubkey(pubkey) - .since(fake_timestamp) + .since(fake_timestamp)) } ListKind::PrivateDirectMessagesUser => { // Get since from cli or use 30 minutes default - let since = if let Some(since) = since { + let since = if let Some(mins) = since { chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(*since as i64)) + .checked_sub_signed(chrono::Duration::minutes(*mins)) .unwrap() .timestamp() } else { @@ -548,10 +539,10 @@ pub fn create_filter(list_kind: ListKind, pubkey: PublicKey, since: Option<&u64> .timestamp() } as u64; // Create filter for fetching privatedirect messages - Filter::new() + Ok(Filter::new() .kind(nostr_sdk::Kind::PrivateDirectMessage) .pubkey(pubkey) - .since(Timestamp::from(since)) + .since(Timestamp::from(since))) } } } @@ -567,19 +558,19 @@ pub async fn fetch_events_list( ) -> Result> { match list_kind { ListKind::Orders => { - let filters = create_filter(list_kind, ctx.mostro_pubkey, None); + let filters = create_filter(list_kind, ctx.mostro_pubkey, None)?; let fetched_events = ctx .client - .fetch_events(filters, Duration::from_secs(15)) + .fetch_events(filters, FETCH_EVENTS_TIMEOUT) .await?; let orders = parse_orders_events(fetched_events, currency, status, kind); Ok(orders.into_iter().map(Event::SmallOrder).collect()) } ListKind::DirectMessagesAdmin => { - let filters = create_filter(list_kind, ctx.mostro_pubkey, None); + let filters = create_filter(list_kind, ctx.mostro_pubkey, None)?; let fetched_events = ctx .client - .fetch_events(filters, Duration::from_secs(15)) + .fetch_events(filters, FETCH_EVENTS_TIMEOUT) .await?; let direct_messages_mostro = parse_dm_events(fetched_events, &ctx.context_keys).await; Ok(direct_messages_mostro @@ -595,10 +586,10 @@ pub async fn fetch_events_list( ListKind::PrivateDirectMessagesUser, trade_key.public_key(), None, - ); + )?; let fetched_user_messages = ctx .client - .fetch_events(filter, Duration::from_secs(15)) + .fetch_events(filter, FETCH_EVENTS_TIMEOUT) .await?; let direct_messages_for_trade_key = parse_dm_events(fetched_user_messages, &trade_key).await; @@ -618,10 +609,10 @@ pub async fn fetch_events_list( for index in 1..=ctx.trade_index { let trade_key = User::get_trade_keys(&ctx.pool, index).await?; let filter = - create_filter(ListKind::DirectMessagesUser, trade_key.public_key(), None); + create_filter(ListKind::DirectMessagesUser, trade_key.public_key(), None)?; let fetched_user_messages = ctx .client - .fetch_events(filter, Duration::from_secs(15)) + .fetch_events(filter, FETCH_EVENTS_TIMEOUT) .await?; let direct_messages_for_trade_key = parse_dm_events(fetched_user_messages, &trade_key).await; @@ -637,10 +628,10 @@ pub async fn fetch_events_list( .collect()) } ListKind::Disputes => { - let filters = create_filter(list_kind, ctx.mostro_pubkey, None); + let filters = create_filter(list_kind, ctx.mostro_pubkey, None)?; let fetched_events = ctx .client - .fetch_events(filters, Duration::from_secs(15)) + .fetch_events(filters, FETCH_EVENTS_TIMEOUT) .await?; let disputes = parse_dispute_events(fetched_events); Ok(disputes.into_iter().map(Event::Dispute).collect()) From be5d5a66821794e39fa8ffb5f6135e59991c0508 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Wed, 17 Sep 2025 16:50:24 +0200 Subject: [PATCH 19/19] feat: introduced some unit testing --- Cargo.lock | 217 ++++++++++++++++++++++++++++++++++--- Cargo.toml | 5 + src/parser/disputes.rs | 3 + src/parser/dms.rs | 3 + src/parser/orders.rs | 3 + src/util.rs | 3 + tests/integration_tests.rs | 71 ++++++++++++ tests/parser_disputes.rs | 50 +++++++++ tests/parser_dms.rs | 19 ++++ tests/parser_orders.rs | 81 ++++++++++++++ 10 files changed, 440 insertions(+), 15 deletions(-) create mode 100644 tests/integration_tests.rs create mode 100644 tests/parser_disputes.rs create mode 100644 tests/parser_dms.rs create mode 100644 tests/parser_orders.rs diff --git a/Cargo.lock b/Cargo.lock index 02cc78c..9afd76f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,28 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-utility" version = "0.3.1" @@ -977,6 +999,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -1036,6 +1064,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "gloo-timers" version = "0.3.0" @@ -1728,13 +1762,15 @@ dependencies = [ "log", "mostro-core", "nostr-sdk", - "openssl", "pretty_env_logger", "reqwest", + "rstest", "serde", "serde_json", + "serial_test", "sqlx", "tokio", + "tokio-test", "uuid", ] @@ -1948,15 +1984,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "openssl-src" -version = "300.5.2+3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" -dependencies = [ - "cc", -] - [[package]] name = "openssl-sys" version = "0.9.109" @@ -1965,7 +1992,6 @@ checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", - "openssl-src", "pkg-config", "vcpkg", ] @@ -2110,6 +2136,15 @@ dependencies = [ "log", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -2242,6 +2277,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.23" @@ -2317,12 +2358,50 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.42" @@ -2423,6 +2502,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.27" @@ -2460,6 +2548,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "secp256k1" version = "0.29.1" @@ -2513,20 +2607,36 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -2557,6 +2667,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3093,6 +3228,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-tungstenite" version = "0.26.2" @@ -3122,6 +3270,36 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ad0b7ae9cfeef5605163839cb9221f453399f15cfb5c10be9885fcf56611f9" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.2" @@ -3736,6 +3914,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index e9b6884..3509092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,3 +46,8 @@ openssl = { version = "0.10.73", features = ["vendored"] } sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio-native-tls"] } bip39 = { version = "2.2.0", features = ["rand"] } dirs = "6.0.0" + +[dev-dependencies] +tokio-test = "0.4" +serial_test = "3.1" +rstest = "0.26.1" \ No newline at end of file diff --git a/src/parser/disputes.rs b/src/parser/disputes.rs index 35bce8c..3e409f1 100644 --- a/src/parser/disputes.rs +++ b/src/parser/disputes.rs @@ -117,3 +117,6 @@ pub fn print_disputes_table(disputes_table: Vec) -> Result { Ok(table.to_string()) } + +#[cfg(test)] +mod tests {} diff --git a/src/parser/dms.rs b/src/parser/dms.rs index 9855157..f4a1466 100644 --- a/src/parser/dms.rs +++ b/src/parser/dms.rs @@ -178,3 +178,6 @@ pub async fn print_direct_messages(dm: &[(Message, u64)], pool: &SqlitePool) -> } Ok(()) } + +#[cfg(test)] +mod tests {} diff --git a/src/parser/orders.rs b/src/parser/orders.rs index 7ccece3..ff6134e 100644 --- a/src/parser/orders.rs +++ b/src/parser/orders.rs @@ -281,3 +281,6 @@ pub fn print_orders_table(orders_table: Vec) -> Result { Ok(table.to_string()) } + +#[cfg(test)] +mod tests {} diff --git a/src/util.rs b/src/util.rs index e6b5477..0d50db1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -661,3 +661,6 @@ pub fn get_mcli_path() -> String { pub async fn run_simple_order_msg(command: Commands, order_id: &Uuid, ctx: &Context) -> Result<()> { execute_send_msg(command, Some(*order_id), ctx, None).await } + +#[cfg(test)] +mod tests {} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..c312fb6 --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,71 @@ +use mostro_client::cli::Context; +use nostr_sdk::prelude::*; +use sqlx::SqlitePool; + +// Helper to create a test context for integration tests +async fn create_test_context() -> anyhow::Result { + let pool = SqlitePool::connect("sqlite::memory:").await?; + + // Generate test keys + let identity_keys = Keys::generate(); + let trade_keys = Keys::generate(); + let context_keys = Keys::generate(); + + // Create a test client + let client = Client::new(identity_keys.clone()); + + // Mock mostro pubkey + let mostro_pubkey = PublicKey::from_hex(&format!("02{}", "1".repeat(62)))?; + + Ok(Context { + client, + identity_keys, + trade_keys, + trade_index: 0, + pool, + context_keys, + mostro_pubkey, + }) +} + +#[tokio::test] +async fn test_context_creation() { + let result = create_test_context().await; + assert!(result.is_ok()); + + let ctx = result.unwrap(); + assert_eq!(ctx.trade_index, 0); +} + +#[tokio::test] +async fn test_context_fields_are_valid() { + let ctx = create_test_context().await.unwrap(); + + // Verify all required fields are present and valid + assert!(!ctx.identity_keys.public_key().to_hex().is_empty()); + assert!(!ctx.identity_keys.public_key().to_hex().is_empty()); + assert!(!ctx.trade_keys.public_key().to_hex().is_empty()); + assert!(!ctx.context_keys.public_key().to_hex().is_empty()); + assert!(!ctx.mostro_pubkey.to_hex().is_empty()); + assert!(!ctx.pool.is_closed()); +} + +#[tokio::test] +async fn test_filter_creation_integration() { + let ctx = create_test_context().await.unwrap(); + + let filter = mostro_client::util::create_filter( + mostro_client::util::ListKind::Orders, + ctx.mostro_pubkey, + None, + ) + .unwrap(); + + assert!(filter.kinds.is_some()); + assert!(filter.authors.is_some()); + assert!(filter + .authors + .as_ref() + .unwrap() + .contains(&ctx.mostro_pubkey)); +} diff --git a/tests/parser_disputes.rs b/tests/parser_disputes.rs new file mode 100644 index 0000000..0f4c30c --- /dev/null +++ b/tests/parser_disputes.rs @@ -0,0 +1,50 @@ +use mostro_client::parser::disputes::{parse_dispute_events, print_disputes_table}; +use mostro_core::prelude::*; +use nostr_sdk::prelude::*; + +fn build_dispute_event(id: uuid::Uuid, status: DisputeStatus) -> nostr_sdk::Event { + let keys = Keys::generate(); + let mut tags = Tags::new(); + tags.push(Tag::custom( + TagKind::Custom("d".into()), + vec![id.to_string()], + )); + tags.push(Tag::custom( + TagKind::Custom("y".into()), + vec!["dispute".to_string()], + )); + tags.push(Tag::custom( + TagKind::Custom("s".into()), + vec![status.to_string()], + )); + EventBuilder::new(nostr_sdk::Kind::TextNote, "") + .tags(tags) + .sign_with_keys(&keys) + .unwrap() +} + +#[test] +fn parse_disputes_empty() { + let filter = Filter::new(); + let events = Events::new(&filter); + let out = parse_dispute_events(events); + assert!(out.is_empty()); +} + +#[test] +fn parse_disputes_basic_and_print() { + let filter = Filter::new(); + let id = uuid::Uuid::new_v4(); + let e = build_dispute_event(id, DisputeStatus::Initiated); + let mut events = Events::new(&filter); + events.insert(e); + let out = parse_dispute_events(events); + assert_eq!(out.len(), 1); + + let printable = out + .into_iter() + .map(mostro_client::util::Event::Dispute) + .collect::>(); + let table = print_disputes_table(printable).expect("table should render"); + assert!(table.contains(&id.to_string())); +} diff --git a/tests/parser_dms.rs b/tests/parser_dms.rs new file mode 100644 index 0000000..ebd590c --- /dev/null +++ b/tests/parser_dms.rs @@ -0,0 +1,19 @@ +use mostro_client::parser::dms::{parse_dm_events, print_direct_messages}; +use mostro_core::prelude::*; +use nostr_sdk::prelude::*; + +#[tokio::test] +async fn parse_dm_empty() { + let keys = Keys::generate(); + let events = Events::new(&Filter::new()); + let out = parse_dm_events(events, &keys).await; + assert!(out.is_empty()); +} + +#[tokio::test] +async fn print_dms_empty() { + let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap(); + let msgs: Vec<(Message, u64)> = Vec::new(); + let res = print_direct_messages(&msgs, &pool).await; + assert!(res.is_ok()); +} diff --git a/tests/parser_orders.rs b/tests/parser_orders.rs new file mode 100644 index 0000000..26d61c9 --- /dev/null +++ b/tests/parser_orders.rs @@ -0,0 +1,81 @@ +use mostro_client::parser::orders::{parse_orders_events, print_orders_table}; +use mostro_core::prelude::*; +use nostr_sdk::prelude::*; + +fn build_order_event( + kind: mostro_core::order::Kind, + status: Status, + fiat: &str, + amount: i64, + fiat_amount: i64, +) -> nostr_sdk::Event { + let keys = Keys::generate(); + let id = uuid::Uuid::new_v4(); + + let mut tags = Tags::new(); + tags.push(Tag::custom( + TagKind::Custom("d".into()), + vec![id.to_string()], + )); + tags.push(Tag::custom( + TagKind::Custom("k".into()), + vec![kind.to_string()], + )); + tags.push(Tag::custom( + TagKind::Custom("f".into()), + vec![fiat.to_string()], + )); + tags.push(Tag::custom( + TagKind::Custom("s".into()), + vec![status.to_string()], + )); + tags.push(Tag::custom( + TagKind::Custom("amt".into()), + vec![amount.to_string()], + )); + tags.push(Tag::custom( + TagKind::Custom("fa".into()), + vec![fiat_amount.to_string()], + )); + + EventBuilder::new(nostr_sdk::Kind::TextNote, "") + .tags(tags) + .sign_with_keys(&keys) + .unwrap() +} + +#[test] +fn parse_orders_empty() { + let filter = Filter::new(); + let events = Events::new(&filter); + let out = parse_orders_events(events, None, None, None); + assert!(out.is_empty()); +} + +#[test] +fn parse_orders_basic_and_print() { + let filter = Filter::new(); + let e = build_order_event( + mostro_core::order::Kind::Sell, + Status::Pending, + "USD", + 100, + 1000, + ); + let mut events = Events::new(&filter); + events.insert(e); + let out = parse_orders_events( + events, + Some("USD".into()), + Some(Status::Pending), + Some(mostro_core::order::Kind::Sell), + ); + assert_eq!(out.len(), 1); + + let printable = out + .into_iter() + .map(mostro_client::util::Event::SmallOrder) + .collect::>(); + let table = print_orders_table(printable).expect("table should render"); + assert!(table.contains("USD")); +}