diff --git a/Cargo.lock b/Cargo.lock index 121be6f..4534eea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1770,9 +1770,9 @@ dependencies = [ [[package]] name = "mostro-core" -version = "0.6.50" +version = "0.6.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8bf32904269e30059c5354a3f379a90ebb6afa7355995d624ec6403062ccd47" +checksum = "10d9108f750c13c52c13c8cc1c447022f404222fa7cd06e576f398c38624bb55" dependencies = [ "argon2", "base64 0.22.1", @@ -1980,9 +1980,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.5.2+3.5.2" +version = "300.5.3+3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2" dependencies = [ "cc", ] diff --git a/Cargo.toml b/Cargo.toml index a60b21c..3344a6a 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.50" +mostro-core = "0.6.56" lnurl-rs = "0.9.0" pretty_env_logger = "0.5.0" openssl = { version = "0.10.73", features = ["vendored"] } diff --git a/README.md b/README.md index b7aa387..d0b9bd4 100644 --- a/README.md +++ b/README.md @@ -4,52 +4,58 @@ Very simple command line interface that show all new replaceable events from [Mostro](https://github.com/MostroP2P/mostro) -## Requirements: +## Requirements 0. You need Rust version 1.64 or higher to compile. 1. You will need a lightning network node -## Install dependencies: +## Install dependencies To compile on Ubuntu/Pop!\_OS, please install [cargo](https://www.rust-lang.org/tools/install), then run the following commands: -``` -$ sudo apt update -$ sudo apt install -y cmake build-essential pkg-config +```bash +sudo apt update +sudo apt install -y cmake build-essential pkg-config ``` ## Install To install you need to fill the env vars (`.env`) on the with your own private key and add a Mostro pubkey. -``` -$ git clone https://github.com/MostroP2P/mostro-cli.git -$ cd mostro-cli -$ cp .env-sample .env -$ cargo run +```bash +git clone https://github.com/MostroP2P/mostro-cli.git +cd mostro-cli +cp .env-sample .env +cargo run ``` -# Usage +## Usage -``` +```bash Commands: - listorders Requests open orders from Mostro pubkey - neworder Create a new buy/sell order on Mostro - takesell Take a sell order from a Mostro pubkey - takebuy Take a buy order from a Mostro pubkey - addinvoice Buyer add a new invoice to receive the payment - getdm Get the latest direct messages from Mostro - fiatsent Send fiat sent message to confirm payment to other user - release Settle the hold invoice and pay to buyer - cancel Cancel a pending order - rate Rate counterpart after a successful trade - dispute Start a dispute - admcancel Cancel an order (only admin) - admsettle Settle a seller's hold invoice (only admin) - admlistdisputes Requests open disputes from Mostro pubkey - admaddsolver Add a new dispute's solver (only admin) - admtakedispute Admin or solver take a Pending dispute (only admin) - help Print this message or the help of the given subcommand(s) + listorders Requests open orders from Mostro pubkey + neworder Create a new buy/sell order on Mostro + takesell Take a sell order from a Mostro pubkey + takebuy Take a buy order from a Mostro pubkey + addinvoice Buyer add a new invoice to receive the payment + getdm Get the latest direct messages + getadmindm Get the latest direct messages for admin + senddm Send direct message to a user + fiatsent Send fiat sent message to confirm payment to other user + release Settle the hold invoice and pay to buyer + cancel Cancel a pending order + rate Rate counterpart after a successful trade + restore Restore session to recover all pending orders and disputes + dispute Start a dispute + admcancel Cancel an order (only admin) + admsettle Settle a seller's hold invoice (only admin) + admlistdisputes Requests open disputes from Mostro pubkey + admaddsolver Add a new dispute's solver (only admin) + admtakedispute Admin or solver take a Pending dispute (only admin) + admsenddm Send gift wrapped direct message to a user (only admin) + conversationkey Get the conversation key for direct messaging with a user + getlasttradeindex Get last trade index of user + help Print this message or the help of the given subcommand(s) Options: -v, --verbose @@ -60,9 +66,9 @@ Options: -V, --version Print version ``` -# Examples +## Examples -``` +```bash $ mostro-cli -m npub1ykvsmrmw2hk7jgxgy64zr8tfkx4nnjhq9eyfxdlg3caha3ph0skq6jr3z0 -r 'wss://nos.lol,wss://relay.damus.io,wss://nostr-pub.wellorder.net,wss://nostr.mutinywallet.com,wss://relay.nostr.band,wss://nostr.cizmar.net,wss://140.f7z.io,wss://nostrrelay.com,wss://relay.nostrr.de' listorders # You can set the env vars to avoid the -m, -n and -r flags diff --git a/src/cli.rs b/src/cli.rs index 8ee84d6..db4a644 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,6 +4,7 @@ pub mod conversation_key; pub mod dm_to_user; pub mod get_dm; pub mod get_dm_user; +pub mod last_trade_index; pub mod list_disputes; pub mod list_orders; pub mod new_order; @@ -20,6 +21,7 @@ use crate::cli::conversation_key::execute_conversation_key; use crate::cli::dm_to_user::execute_dm_to_user; use crate::cli::get_dm::execute_get_dm; use crate::cli::get_dm_user::execute_get_dm_user; +use crate::cli::last_trade_index::execute_last_trade_index; use crate::cli::list_disputes::execute_list_disputes; use crate::cli::list_orders::execute_list_orders; use crate::cli::new_order::execute_new_order; @@ -291,6 +293,8 @@ pub enum Commands { #[arg(short, long)] pubkey: String, }, + /// Get last trade index of user + GetLastTradeIndex {}, } fn get_env_var(cli: &Cli) { @@ -415,9 +419,12 @@ 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(), Some(*order_id), ctx).await + } + // Last trade index commands + Commands::GetLastTradeIndex {} => { + execute_last_trade_index(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await } - // DM commands with pubkey parsing Commands::SendDm { pubkey, @@ -489,11 +496,11 @@ impl Commands { // DM retrieval commands Commands::GetDm { since, from_user } => { - execute_get_dm(Some(since), false, from_user, ctx).await + execute_get_dm(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), true, from_user, ctx).await + execute_get_dm(since, true, from_user, ctx).await } // Admin commands diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index ebf4d61..266bb5e 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -1,4 +1,4 @@ -use crate::util::{send_dm, wait_for_dm}; +use crate::util::{print_dm_events, send_dm, wait_for_dm}; use crate::{cli::Context, db::Order, lightning::is_valid_invoice}; use anyhow::Result; use lnurl::lightning_address::LightningAddress; @@ -8,11 +8,14 @@ use std::str::FromStr; use uuid::Uuid; pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) -> Result<()> { + // Get order from order id let order = Order::get_by_id(&ctx.pool, &order_id.to_string()).await?; + // Get trade keys of specific order let trade_keys = order .trade_keys .clone() .ok_or(anyhow::anyhow!("Missing trade keys"))?; + let order_trade_keys = Keys::parse(&trade_keys)?; println!( "Order trade keys: {:?}", @@ -52,45 +55,22 @@ 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?; + // Send the DM + let sent_message = send_dm( + &ctx.client, + Some(&ctx.identity_keys), + &order_trade_keys, + &ctx.mostro_pubkey, + message_json, + None, + false, + ); - // Clone the keys and client for the async call - let identity_keys_clone = ctx.identity_keys.clone(); - let client_clone = ctx.client.clone(); - let mostro_pubkey_clone = ctx.mostro_pubkey; - let order_trade_keys_clone = order_trade_keys.clone(); + // Wait for the DM to be sent from mostro + let recv_event = wait_for_dm(ctx, Some(&order_trade_keys), sent_message).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), - &order_trade_keys, - &mostro_pubkey_clone, - message_json, - None, - false, - ) - .await; - }); + // Parse the incoming DM + print_dm_events(recv_event, request_id, ctx, Some(&order_trade_keys)).await?; - // Wait for the DM to be sent from mostro and update the order - wait_for_dm( - &ctx.client, - &order_trade_keys_clone, - request_id, - None, - Some(order), - &ctx.pool, - ) - .await?; Ok(()) } diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index d5edf38..fce1133 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -8,7 +8,7 @@ use crate::{ }; pub async fn execute_get_dm( - since: Option<&i64>, + since: &i64, admin: bool, from_user: &bool, ctx: &Context, @@ -22,7 +22,8 @@ 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, Some(since)).await?; // Extract (Message, u64) tuples from Event::MessageTuple variants let mut dm_events: Vec<(Message, u64)> = Vec::new(); diff --git a/src/cli/last_trade_index.rs b/src/cli/last_trade_index.rs new file mode 100644 index 0000000..fe2aaf4 --- /dev/null +++ b/src/cli/last_trade_index.rs @@ -0,0 +1,60 @@ +use anyhow::Result; +use mostro_core::prelude::*; +use nostr_sdk::prelude::*; + +use crate::{ + cli::Context, + parser::{dms::print_commands_results, parse_dm_events}, + util::{send_dm, wait_for_dm}, +}; + +pub async fn execute_last_trade_index( + identity_keys: &Keys, + mostro_key: PublicKey, + ctx: &Context, +) -> Result<()> { + let kind = MessageKind::new(None, None, None, Action::LastTradeIndex, None); + let last_trade_index_message = Message::Restore(kind); + let message_json = last_trade_index_message + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; + + // Send the last trade index message to Mostro server + let sent_message = send_dm( + &ctx.client, + Some(identity_keys), + identity_keys, + &mostro_key, + message_json, + None, + false, + ); + + // Log the sent message + println!( + "Sent request to Mostro to get last trade index of user {}", + identity_keys.public_key() + ); + + // Wait for incoming DM + let recv_event = wait_for_dm(ctx, Some(identity_keys), sent_message).await?; + + // Parse the incoming DM + let messages = parse_dm_events(recv_event, identity_keys, None).await; + if let Some((message, _, _)) = messages.first() { + let message = message.get_inner_message_kind(); + if message.action == Action::LastTradeIndex { + print_commands_results(message, None, ctx).await? + } else { + return Err(anyhow::anyhow!( + "Received response with mismatched action. Expected: {:?}, Got: {:?}", + Action::LastTradeIndex, + message.action + )); + } + } else { + return Err(anyhow::anyhow!("No response received from Mostro")); + } + + Ok(()) +} diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index 4bf5ed7..7cc1d6a 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -1,9 +1,8 @@ use crate::cli::Context; use crate::parser::orders::print_order_preview; -use crate::util::{send_dm, uppercase_first, wait_for_dm}; +use crate::util::{print_dm_events, send_dm, uppercase_first, wait_for_dm}; use anyhow::Result; use mostro_core::prelude::*; -use nostr_sdk::prelude::*; use std::collections::HashMap; use std::io::{stdin, stdout, BufRead, Write}; use std::process; @@ -119,7 +118,8 @@ pub async fn execute_new_order( // Send dm to receiver pubkey println!( - "SENDING DM with trade keys: {:?}", + "SENDING DM with trade index: {} and trade keys: {:?}", + ctx.trade_index, ctx.trade_keys.public_key().to_hex() ); @@ -128,47 +128,22 @@ pub async fn execute_new_order( .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - // Clone the keys and client for the async call - 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(ctx.trade_keys.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?; - - // 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_pubkey_clone, - message_json, - None, - false, - ) - .await; - }); - - // Wait for the DM to be sent from mostro - wait_for_dm( + // Send the DM + let sent_message = send_dm( &ctx.client, + Some(&ctx.identity_keys), &ctx.trade_keys, - request_id, - Some(ctx.trade_index), + &ctx.mostro_pubkey, + message_json, None, - &ctx.pool, - ) - .await?; + false, + ); + + // Wait for the DM to be sent from mostro + let recv_event = wait_for_dm(ctx, None, sent_message).await?; + + // Parse the incoming DM + print_dm_events(recv_event, request_id, ctx, None).await?; Ok(()) } diff --git a/src/cli/send_msg.rs b/src/cli/send_msg.rs index 6701b59..1ae2dce 100644 --- a/src/cli/send_msg.rs +++ b/src/cli/send_msg.rs @@ -1,6 +1,6 @@ use crate::cli::{Commands, Context}; use crate::db::{Order, User}; -use crate::util::{send_dm, wait_for_dm}; +use crate::util::{print_dm_events, send_dm, wait_for_dm}; use anyhow::Result; use mostro_core::prelude::*; @@ -27,11 +27,12 @@ pub async fn execute_send_msg( } }; + // Printout command information println!( - "Sending {} command for order {:?} to mostro pubId {}", + "Sending {} command for order {} to mostro pubId {}", requested_action, - order_id.as_ref(), - &ctx.mostro_pubkey + order_id.unwrap(), + ctx.mostro_pubkey ); // Determine payload @@ -58,53 +59,36 @@ 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 = ctx.identity_keys.to_owned(); if let Some(order_id) = order_id { 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)?; - // 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)); - // Subscribe to gift wrap events - 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( + + // Send DM + let sent_message = send_dm( &ctx.client, - Some(&idkey), + Some(&ctx.identity_keys), &trade_keys, &ctx.mostro_pubkey, 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( - &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 incoming DM + let recv_event = wait_for_dm(ctx, Some(&trade_keys), sent_message).await?; + + // Parse the incoming DM + print_dm_events(recv_event, request_id, ctx, Some(&trade_keys)).await?; } } - Ok(()) } diff --git a/src/cli/take_order.rs b/src/cli/take_order.rs index 62fe4f5..17e0d72 100644 --- a/src/cli/take_order.rs +++ b/src/cli/take_order.rs @@ -1,13 +1,12 @@ 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::cli::Context; use crate::lightning::is_valid_invoice; -use crate::util::{send_dm, wait_for_dm}; +use crate::util::{print_dm_events, send_dm, wait_for_dm}; /// Create payload based on action type and parameters fn create_take_order_payload( @@ -71,6 +70,7 @@ pub async fn execute_take_order( // Create payload based on action type let payload = create_take_order_payload(action.clone(), invoice, amount)?; + // Create request id let request_id = Uuid::new_v4().as_u128() as u64; // Create message @@ -84,7 +84,8 @@ pub async fn execute_take_order( // Send dm to receiver pubkey println!( - "SENDING DM with trade keys: {:?}", + "SENDING DM with trade index: {} and trade keys: {:?}", + ctx.trade_index, ctx.trade_keys.public_key().to_hex() ); @@ -92,57 +93,23 @@ pub async fn execute_take_order( .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - // Clone the keys and client for the async call - 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(ctx.trade_keys.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?; - - // Spawn a new task to send the DM + // 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_pubkey_clone, - message_json, - None, - false, - ) - .await; - }); - - // For take_sell, add an additional subscription with timestamp filtering - if action == Action::TakeSell { - let subscription = Filter::new() - .pubkey(ctx.trade_keys.public_key()) - .kind(nostr_sdk::Kind::GiftWrap) - .since(Timestamp::from(chrono::Utc::now().timestamp() as u64)) - .limit(0); - - ctx.client.subscribe(subscription, None).await?; - } - - // Wait for the DM to be sent from mostro - wait_for_dm( + let sent_message = send_dm( &ctx.client, + Some(&ctx.identity_keys), &ctx.trade_keys, - request_id, - Some(ctx.trade_index), + &ctx.mostro_pubkey, + message_json, None, - &ctx.pool, - ) - .await?; + false, + ); + + // Wait for the DM to be sent from mostro + let recv_event = wait_for_dm(ctx, None, sent_message).await?; + + // Parse the incoming DM + print_dm_events(recv_event, request_id, ctx, None).await?; Ok(()) } diff --git a/src/db.rs b/src/db.rs index f5d9e04..4493af7 100644 --- a/src/db.rs +++ b/src/db.rs @@ -306,35 +306,93 @@ impl Order { expires_at: None, }; + // Try insert; if id already exists, perform an update instead + let insert_result = order.insert_db(pool).await; + + if let Err(e) = insert_result { + // If the error is due to unique constraint (id already present), update instead + // SQLite uses error code 1555 (constraint failed) or 2067 (unique constraint failed) + let is_unique_violation = match e.as_database_error() { + Some(db_err) => { + let code = db_err.code().map(|c| c.to_string()).unwrap_or_default(); + code == "1555" || code == "2067" + } + None => false, + }; + + if is_unique_violation { + order.update_db(pool).await?; + } else { + return Err(e.into()); + } + } + + Ok(order) + } + + async fn insert_db(&self, pool: &SqlitePool) -> Result<(), sqlx::Error> { sqlx::query( r#" - INSERT INTO orders (id, kind, status, amount, min_amount, max_amount, - fiat_code, fiat_amount, payment_method, premium, trade_keys, - counterparty_pubkey, is_mine, buyer_invoice, request_id, created_at, expires_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - "#, + INSERT INTO orders (id, kind, status, amount, min_amount, max_amount, + fiat_code, fiat_amount, payment_method, premium, trade_keys, + counterparty_pubkey, is_mine, buyer_invoice, request_id, created_at, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "#, ) - .bind(&order.id) - .bind(&order.kind) - .bind(&order.status) - .bind(order.amount) - .bind(order.min_amount) - .bind(order.max_amount) - .bind(&order.fiat_code) - .bind(order.fiat_amount) - .bind(&order.payment_method) - .bind(order.premium) - .bind(&order.trade_keys) - .bind(&order.counterparty_pubkey) - .bind(order.is_mine) - .bind(&order.buyer_invoice) - .bind(order.request_id) - .bind(order.created_at) - .bind(order.expires_at) + .bind(&self.id) + .bind(&self.kind) + .bind(&self.status) + .bind(self.amount) + .bind(self.min_amount) + .bind(self.max_amount) + .bind(&self.fiat_code) + .bind(self.fiat_amount) + .bind(&self.payment_method) + .bind(self.premium) + .bind(&self.trade_keys) + .bind(&self.counterparty_pubkey) + .bind(self.is_mine) + .bind(&self.buyer_invoice) + .bind(self.request_id) + .bind(self.created_at) + .bind(self.expires_at) .execute(pool) - .await?; + .await? + .rows_affected(); + Ok(()) + } - Ok(order) + async fn update_db(&self, pool: &SqlitePool) -> Result<(), sqlx::Error> { + sqlx::query( + r#" + UPDATE orders + SET kind = ?, status = ?, amount = ?, min_amount = ?, max_amount = ?, + fiat_code = ?, fiat_amount = ?, payment_method = ?, premium = ?, trade_keys = ?, + counterparty_pubkey = ?, is_mine = ?, buyer_invoice = ?, request_id = ?, created_at = ?, expires_at = ? + WHERE id = ? + "#, + ) + .bind(&self.kind) + .bind(&self.status) + .bind(self.amount) + .bind(self.min_amount) + .bind(self.max_amount) + .bind(&self.fiat_code) + .bind(self.fiat_amount) + .bind(&self.payment_method) + .bind(self.premium) + .bind(&self.trade_keys) + .bind(&self.counterparty_pubkey) + .bind(self.is_mine) + .bind(&self.buyer_invoice) + .bind(self.request_id) + .bind(self.created_at) + .bind(self.expires_at) + .bind(&self.id) + .execute(pool) + .await? + .rows_affected(); + Ok(()) } // Setters encadenables diff --git a/src/parser/dms.rs b/src/parser/dms.rs index f4a1466..26f8af0 100644 --- a/src/parser/dms.rs +++ b/src/parser/dms.rs @@ -8,10 +8,210 @@ use mostro_core::prelude::*; use nip44::v2::{decrypt_to_bytes, ConversationKey}; use nostr_sdk::prelude::*; -use crate::db::{Order, User}; +use crate::{ + cli::Context, + db::{Order, User}, + util::save_order, +}; use sqlx::SqlitePool; -pub async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message, u64, PublicKey)> { +/// Execute logic of command answer +pub async fn print_commands_results( + message: &MessageKind, + mut order: Option, + ctx: &Context, +) -> Result<()> { + // Do the logic for the message response + match message.action { + Action::NewOrder => { + if let Some(Payload::Order(order)) = message.payload.as_ref() { + if let Some(req_id) = message.request_id { + if let Err(e) = save_order( + order.clone(), + &ctx.trade_keys, + req_id, + ctx.trade_index, + &ctx.pool, + ) + .await + { + return Err(anyhow::anyhow!("Failed to save order: {}", e)); + } + Ok(()) + } else { + Err(anyhow::anyhow!("No request id found in message")) + } + } else { + Err(anyhow::anyhow!("No order found in message")) + } + } + // 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() { + match order + .set_status(Status::WaitingPayment.to_string()) + .save(&ctx.pool) + .await + { + Ok(_) => println!("Order status updated"), + Err(e) => println!("Failed to update order status: {}", e), + } + Ok(()) + } else { + Err(anyhow::anyhow!("No order found in message")) + } + } + // 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 + ); + if let Some(req_id) = message.request_id { + // Save the order + if let Err(e) = save_order( + order.clone(), + &ctx.trade_keys, + req_id, + ctx.trade_index, + &ctx.pool, + ) + .await + { + return Err(anyhow::anyhow!("Failed to save order: {}", e)); + } + } else { + return Err(anyhow::anyhow!("No request id found in message")); + } + Ok(()) + } else { + Err(anyhow::anyhow!("No order found in message")) + } + } + // 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 { + if let Some(req_id) = message.request_id { + let store_order = order.clone(); + // Save the order + if let Err(e) = save_order( + store_order, + &ctx.trade_keys, + req_id, + ctx.trade_index, + &ctx.pool, + ) + .await + { + println!("Failed to save order: {}", e); + return Err(anyhow::anyhow!("Failed to save order: {}", e)); + } + } else { + return Err(anyhow::anyhow!("No request id found in message")); + } + } else { + return Err(anyhow::anyhow!("No request id found in message")); + } + } + Ok(()) + } + Action::CantDo => match message.payload { + Some(Payload::CantDo(Some( + CantDoReason::OutOfRangeFiatAmount | CantDoReason::OutOfRangeSatsAmount, + ))) => Err(anyhow::anyhow!( + "Amount is outside the allowed range. Please check the order's min/max limits." + )), + Some(Payload::CantDo(Some(CantDoReason::PendingOrderExists))) => Err(anyhow::anyhow!( + "A pending order already exists. Please wait for it to be filled or canceled." + )), + Some(Payload::CantDo(Some(CantDoReason::InvalidTradeIndex))) => Err(anyhow::anyhow!( + "Invalid trade index. Please synchronize the trade index with mostro" + )), + Some(Payload::CantDo(Some(CantDoReason::InvalidFiatCurrency))) => Err(anyhow::anyhow!( + " + Invalid currency" + )), + _ => Err(anyhow::anyhow!("Unknown reason: {:?}", message.payload)), + }, + // this is the case where the user cancels the order + Action::Canceled => { + if let Some(order_id) = &message.id { + // Acquire database connection + // Verify order exists before deletion + if Order::get_by_id(&ctx.pool, &order_id.to_string()) + .await + .is_ok() + { + if let Err(e) = Order::delete_by_id(&ctx.pool, &order_id.to_string()).await { + return Err(anyhow::anyhow!("Failed to delete order: {}", e)); + } + // Release database connection + println!("Order {} canceled!", order_id); + Ok(()) + } else { + Err(anyhow::anyhow!("Order not found: {}", order_id)) + } + } else { + Err(anyhow::anyhow!("No order id found in message")) + } + } + Action::Rate => { + println!("Sats released!"); + println!("You can rate the counterpart now"); + Ok(()) + } + Action::FiatSentOk => { + if let Some(order_id) = &message.id { + println!("Fiat sent message for order {:?} received", order_id); + println!("Waiting for sats release from seller"); + Ok(()) + } else { + Err(anyhow::anyhow!("No order id found in message")) + } + } + Action::LastTradeIndex => { + if let Some(last_trade_index) = message.trade_index { + println!("Last trade index message received: {}", last_trade_index); + match User::get(&ctx.pool).await { + Ok(mut user) => { + user.set_last_trade_index(last_trade_index); + if let Err(e) = user.save(&ctx.pool).await { + println!("Failed to update user: {}", e); + } + } + Err(_) => return Err(anyhow::anyhow!("Failed to get user")), + } + Ok(()) + } else { + Err(anyhow::anyhow!("No trade index found in message")) + } + } + Action::HoldInvoicePaymentSettled => { + println!("Hold invoice payment settled"); + Ok(()) + } + _ => Err(anyhow::anyhow!("Unknown action: {:?}", message.action)), + } +} + +pub async fn parse_dm_events( + events: Events, + pubkey: &Keys, + since: Option<&i64>, +) -> Vec<(Message, u64, PublicKey)> { let mut id_set = HashSet::::new(); let mut direct_messages: Vec<(Message, u64, PublicKey)> = Vec::new(); @@ -75,17 +275,17 @@ pub async fn parse_dm_events(events: Events, pubkey: &Keys) -> Vec<(Message, u64 } _ => continue, }; + // check if the message is older than the since time if it is, skip it + if let Some(since_time) = since { + // Calculate since time from now in minutes subtracting the since time + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(*since_time)) + .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"); + if created_at.as_u64() < since_time { continue; } - }; - if created_at.as_u64() < since_time { - continue; } direct_messages.push((message, created_at.as_u64(), dm.pubkey)); } diff --git a/src/util.rs b/src/util.rs index 1f4fe50..e57fd80 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,7 @@ use crate::cli::send_msg::execute_send_msg; use crate::cli::{Commands, Context}; use crate::db::{Order, User}; +use crate::parser::dms::print_commands_results; use crate::parser::{parse_dispute_events, parse_dm_events, parse_orders_events}; use anyhow::{Error, Result}; use base64::engine::general_purpose; @@ -11,6 +12,7 @@ use mostro_core::prelude::*; use nip44::v2::{encrypt_to_bytes, ConversationKey}; use nostr_sdk::prelude::*; use sqlx::SqlitePool; +use std::future::Future; use std::time::Duration; use std::{fs, path::Path}; use uuid::Uuid; @@ -97,7 +99,7 @@ pub async fn save_order( order: SmallOrder, trade_keys: &Keys, request_id: u64, - trade_index: Option, + trade_index: i64, pool: &SqlitePool, ) -> Result<()> { if let Ok(order) = Order::new(pool, order, trade_keys, Some(request_id as i64)).await { @@ -106,14 +108,6 @@ 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 { @@ -130,154 +124,61 @@ pub async fn save_order( } /// 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: Option, - mut order: Option, - pool: &SqlitePool, -) -> anyhow::Result<()> { - let mut notifications = client.notifications(); - - 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 { - 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() { - 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(()); - } - } - // 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() { - 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), - } - return Ok(()); - } - } - // 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 - ); - // Save the order - 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(()); - } - } - // 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 the order - 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(()); - } - } - 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(()); - } - } +pub async fn wait_for_dm( + ctx: &Context, + order_trade_keys: Option<&Keys>, + sent_message: F, +) -> anyhow::Result +where + F: Future> + Send, +{ + // Get correct trade keys to wait for + let trade_keys = order_trade_keys.unwrap_or(&ctx.trade_keys); + // Get notifications from client + let mut notifications = ctx.client.notifications(); + // Create subscription + let opts = + SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEventsAfterEOSE(1)); + // 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 subscription with exit policy of just waiting for 1 event + ctx.client.subscribe(subscription, Some(opts)).await?; + + // Await the sent message + sent_message.await?; + + // Wait for event + let event = tokio::time::timeout(FETCH_EVENTS_TIMEOUT, async move { + loop { + match notifications.recv().await { + Ok(notification) => { + match notification { + RelayPoolNotification::Event { event, .. } => { + // Return event + return Ok(*event); } - // this is the case where the user cancels the order - Action::Canceled => { - if let Some(order_id) = &message.id { - // Acquire database connection - // Verify order exists before deletion - if Order::get_by_id(pool, &order_id.to_string()).await.is_ok() { - 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(()); - } else { - println!("Order not found: {}", order_id); - return Err(()); - } - } + _ => { + // Continue waiting for a valid event + continue; } - _ => {} - } } } + Err(e) => { + return Err(anyhow::anyhow!("Error receiving notification: {:?}", e)); + } + } } - } - Ok(()) }) - .await { - 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")) - } + .await? + .map_err(|_| anyhow::anyhow!("Timeout waiting for DM or gift wrap event"))?; + + // Convert event to events + let mut events = Events::default(); + events.insert(event); + Ok(events) } #[derive(Debug, Clone, Copy)] @@ -554,7 +455,7 @@ pub async fn fetch_events_list( currency: Option, kind: Option, ctx: &Context, - _since: Option<&i64>, + since: Option<&i64>, ) -> Result> { match list_kind { ListKind::Orders => { @@ -572,7 +473,8 @@ pub async fn fetch_events_list( .client .fetch_events(filters, FETCH_EVENTS_TIMEOUT) .await?; - let direct_messages_mostro = parse_dm_events(fetched_events, &ctx.context_keys).await; + let direct_messages_mostro = + parse_dm_events(fetched_events, &ctx.context_keys, since).await; Ok(direct_messages_mostro .into_iter() .map(|(message, timestamp, _)| Event::MessageTuple(Box::new((message, timestamp)))) @@ -592,7 +494,7 @@ pub async fn fetch_events_list( .fetch_events(filter, FETCH_EVENTS_TIMEOUT) .await?; let direct_messages_for_trade_key = - parse_dm_events(fetched_user_messages, &trade_key).await; + parse_dm_events(fetched_user_messages, &trade_key, since).await; direct_messages.extend( direct_messages_for_trade_key .into_iter() @@ -606,6 +508,7 @@ pub async fn fetch_events_list( } ListKind::DirectMessagesUser => { let mut direct_messages: Vec<(Message, u64)> = Vec::new(); + for index in 1..=ctx.trade_index { let trade_key = User::get_trade_keys(&ctx.pool, index).await?; let filter = @@ -615,7 +518,7 @@ pub async fn fetch_events_list( .fetch_events(filter, FETCH_EVENTS_TIMEOUT) .await?; let direct_messages_for_trade_key = - parse_dm_events(fetched_user_messages, &trade_key).await; + parse_dm_events(fetched_user_messages, &trade_key, since).await; direct_messages.extend( direct_messages_for_trade_key .into_iter() @@ -658,8 +561,12 @@ 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: Option, + ctx: &Context, +) -> Result<()> { + execute_send_msg(command, order_id, ctx, None).await } // helper (place near other CLI utils) @@ -677,5 +584,32 @@ pub async fn admin_send_dm(ctx: &Context, msg: String) -> anyhow::Result<()> { Ok(()) } +pub async fn print_dm_events( + recv_event: Events, + request_id: u64, + ctx: &Context, + order_trade_keys: Option<&Keys>, +) -> Result<()> { + // Get the trade keys + let trade_keys = order_trade_keys.unwrap_or(&ctx.trade_keys); + // Parse the incoming DM + let messages = parse_dm_events(recv_event, trade_keys, None).await; + if let Some((message, _, _)) = messages.first() { + let message = message.get_inner_message_kind(); + if message.request_id == Some(request_id) { + print_commands_results(message, None, ctx).await?; + } else { + return Err(anyhow::anyhow!( + "Received response with mismatched request_id. Expected: {}, Got: {:?}", + request_id, + message.request_id + )); + } + } else { + return Err(anyhow::anyhow!("No response received from Mostro")); + } + Ok(()) +} + #[cfg(test)] mod tests {} diff --git a/tests/parser_dms.rs b/tests/parser_dms.rs index ebd590c..7ce9e6e 100644 --- a/tests/parser_dms.rs +++ b/tests/parser_dms.rs @@ -6,7 +6,7 @@ use nostr_sdk::prelude::*; async fn parse_dm_empty() { let keys = Keys::generate(); let events = Events::new(&Filter::new()); - let out = parse_dm_events(events, &keys).await; + let out = parse_dm_events(events, &keys, None).await; assert!(out.is_empty()); }