diff --git a/Cargo.lock b/Cargo.lock index 75c92071..2471597a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,14 +376,14 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.4" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788902099d47c8682efe6a7afb01c8d58b9794ba66c06affd81c3d6b560743eb" +checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", "bech32 0.11.0", "bitcoin-internals 0.3.0", - "bitcoin-io", + "bitcoin-io 0.1.3", "bitcoin-units", "bitcoin_hashes 0.14.0", "hex-conservative 0.2.1", @@ -407,12 +407,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin-internals" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b854212e29b96c8f0fe04cab11d57586c8f3257de0d146c76cb3b42b3eb9118" + [[package]] name = "bitcoin-io" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +[[package]] +name = "bitcoin-io" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26792cd2bf245069a1c5acb06aa7ad7abe1de69b507c90b490bca81e0665d0ee" +dependencies = [ + "bitcoin-internals 0.4.0", +] + [[package]] name = "bitcoin-units" version = "0.1.2" @@ -439,11 +454,21 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" dependencies = [ - "bitcoin-io", + "bitcoin-io 0.1.3", "hex-conservative 0.2.1", "serde", ] +[[package]] +name = "bitcoin_hashes" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0982261c82a50d89d1a411602afee0498b3e0debe3d36693f0c661352809639" +dependencies = [ + "bitcoin-io 0.2.0", + "hex-conservative 0.3.0", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -552,12 +577,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chacha20" version = "0.9.1" @@ -1283,6 +1302,15 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-conservative" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afe881d0527571892c4034822e59bb10c6c991cce6abe8199b6f5cf10766f55" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex_lit" version = "0.1.1" @@ -1439,7 +1467,6 @@ dependencies = [ "tokio", "tokio-rustls 0.26.0", "tower-service", - "webpki-roots 0.26.6", ] [[package]] @@ -1806,23 +1833,11 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" -[[package]] -name = "lnurl-pay" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "536e7c782167a2d48346ca0b2677fad19eaef20f19a4ab868e4d5b96ca879def" -dependencies = [ - "bech32 0.11.0", - "reqwest", - "serde", - "serde_json", -] - [[package]] name = "lnurl-rs" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2951a7783efa798febff75dfd9df4716c3ddc5bea969f132b282972a36bc7d8f" +checksum = "41eacdd87b675792f7752f3dd0937a00241a504c3956c47f72986490662e1db4" dependencies = [ "aes", "anyhow", @@ -1951,6 +1966,7 @@ name = "mostro" version = "0.12.8" dependencies = [ "anyhow", + "bitcoin", "chrono", "clap", "config", @@ -1975,11 +1991,13 @@ dependencies = [ [[package]] name = "mostro-core" -version = "0.6.11" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3416a4220576cecb437ff5f22df995d296e022ab85ed541c385ba7bfc97d14bc" +checksum = "365f4109979a283c0f198befaa9a518edb346873ead95fca742fb7cc5eb65c81" dependencies = [ "anyhow", + "bitcoin", + "bitcoin_hashes 0.15.0", "chrono", "nostr-sdk", "serde", @@ -2037,11 +2055,10 @@ dependencies = [ [[package]] name = "nostr" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ad56c1d9a59f4edc46b17bc64a217b38b99baefddc0080f85ad98a0855336d" +checksum = "8aad4b767bbed24ac5eb4465bfb83bc1210522eb99d67cf4e547ec2ec7e47786" dependencies = [ - "aes", "async-trait", "base64 0.22.1", "bech32 0.11.0", @@ -2052,26 +2069,21 @@ dependencies = [ "chacha20poly1305", "getrandom", "instant", - "js-sys", "negentropy 0.3.1", "negentropy 0.4.3", "once_cell", - "reqwest", "scrypt", "serde", "serde_json", "unicode-normalization", "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", ] [[package]] name = "nostr-database" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1859abebf78d7d9e945b20c8faaf710c9db905adeb148035b803ae45792dbebe" +checksum = "23696338d51e45cd44e061823847f4b0d1d362eca80d5033facf9c184149f72f" dependencies = [ "async-trait", "lru", @@ -2083,9 +2095,9 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e39cfcb30cab86b30ca9acba89f5ccb25a4142a5dc5fcfbf3edf34b204ddd7c7" +checksum = "15fcc6e3f0ca54d0fc779009bc5f2684cea9147be3b6aa68a7d301ea590f95f5" dependencies = [ "async-utility", "async-wsocket", @@ -2102,34 +2114,20 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4739ed15ff81a0e474d79b38c3eb481ff5f968c1865f38ba46852daf6f6495e" +checksum = "491221fc89b1aa189a0de640127127d68b4e7c5c1d44371b04d9a6d10694b5af" dependencies = [ "async-utility", "atomic-destructor", - "lnurl-pay", "nostr", "nostr-database", "nostr-relay-pool", - "nostr-zapper", - "nwc", "thiserror", "tokio", "tracing", ] -[[package]] -name = "nostr-zapper" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9709ecf8050bbe4ecf0e5efda2f25b690bb1761fc504e05654621ba9e568a8" -dependencies = [ - "async-trait", - "nostr", - "thiserror", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2149,21 +2147,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "nwc" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5f98bcaf232b3ec48e018792ca7bc2b90e7520d001a07b8218a9e76a03fda2" -dependencies = [ - "async-trait", - "async-utility", - "nostr", - "nostr-relay-pool", - "nostr-zapper", - "thiserror", - "tracing", -] - [[package]] name = "object" version = "0.36.5" @@ -2533,55 +2516,6 @@ dependencies = [ "prost", ] -[[package]] -name = "quinn" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" -dependencies = [ - "bytes", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls 0.23.16", - "socket2", - "thiserror", - "tokio", - "tracing", -] - -[[package]] -name = "quinn-proto" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" -dependencies = [ - "bytes", - "rand", - "ring 0.17.8", - "rustc-hash", - "rustls 0.23.16", - "slab", - "thiserror", - "tinyvec", - "tracing", -] - -[[package]] -name = "quinn-udp" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.37" @@ -2710,10 +2644,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "quinn", - "rustls 0.23.16", "rustls-pemfile 2.2.0", - "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -2721,14 +2652,12 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.0", "tokio-socks", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.26.6", "windows-registry", ] @@ -2790,12 +2719,6 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" - [[package]] name = "rustix" version = "0.38.39" diff --git a/Cargo.toml b/Cargo.toml index 596ff7a4..1279c2e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ anyhow = "1.0.89" chrono = "0.4.35" easy-hasher = "2.2.1" lightning-invoice = { version = "0.32.0", features = ["std"] } -nostr-sdk = "0.36.0" +nostr-sdk = { version = "0.37.0", features = ["nip59"] } serde = { version = "1.0.210" } serde_json = "1.0.128" sqlx = { version = "0.6.2", features = [ @@ -38,11 +38,12 @@ uuid = { version = "1.8.0", features = [ "serde", ] } reqwest = { version = "0.12.1", features = ["json"] } -mostro-core = { version = "0.6.11", features = ["sqlx"] } +mostro-core = { version = "0.6.18", features = ["sqlx"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } config = "0.14.0" clap = { version = "4.5.19", features = ["derive"] } -lnurl-rs = "0.8.0" +lnurl-rs = "0.9.0" openssl = { version = "0.10.66", features = ["vendored"] } once_cell = "1.20.2" +bitcoin = "0.32.5" diff --git a/migrations/20221222153301_orders.sql b/migrations/20221222153301_orders.sql index 12bb8939..885bb396 100644 --- a/migrations/20221222153301_orders.sql +++ b/migrations/20221222153301_orders.sql @@ -35,5 +35,7 @@ CREATE TABLE IF NOT EXISTS orders ( seller_sent_rate integer default 0, payment_attempts integer default 0, failed_payment integer default 0, - expires_at integer not null -); \ No newline at end of file + expires_at integer not null, + trade_index_seller integer default 0, + trade_index_buyer integer default 0 +); diff --git a/migrations/20231005195154_users.sql b/migrations/20231005195154_users.sql index 47838581..f3941c40 100644 --- a/migrations/20231005195154_users.sql +++ b/migrations/20231005195154_users.sql @@ -1,9 +1,15 @@ CREATE TABLE IF NOT EXISTS users ( id char(36) primary key not null, - pubkey char(64) not null, + pubkey char(64) unique not null, is_admin integer not null default 0, is_solver integer not null default 0, is_banned integer not null default 0, category integer not null default 0, + last_trade_index integer not null default 0, + total_reviews integer not null default 0, + total_rating integer not null default 0, + last_rating integer not null default 0, + max_rating integer not null default 0, + min_rating integer not null default 0, created_at integer not null ); \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index b763d0eb..2f8e1b86 100644 --- a/src/app.rs +++ b/src/app.rs @@ -34,21 +34,82 @@ use crate::app::take_sell::take_sell_action; // Core functionality imports use crate::lightning::LndConnector; use crate::nip59::unwrap_gift_wrap; +use crate::util::send_cant_do_msg; use crate::Settings; // External dependencies +use crate::db::is_user_present; use anyhow::Result; -use mostro_core::message::{Action, Message}; +use mostro_core::message::{Action, CantDoReason, Message}; +use mostro_core::user::User; use nostr_sdk::prelude::*; use sqlx::{Pool, Sqlite}; +use sqlx_crud::Crud; use std::sync::Arc; use tokio::sync::Mutex; - /// Helper function to log warning messages for action errors fn warning_msg(action: &Action, e: anyhow::Error) { tracing::warn!("Error in {} with context {}", action, e); } +/// Function to check if a user is present in the database and update or create their trade index. +/// +/// This function performs the following tasks: +/// 1. It checks if the action associated with the incoming message is related to trading (NewOrder, TakeBuy, or TakeSell). +/// 2. If the user is found in the database, it verifies the trade index and the signature of the message. +/// - If valid, it updates the user's trade index. +/// - If invalid, it logs a warning and sends a message indicating the issue. +/// 3. If the user is not found, it creates a new user entry with the provided trade index if applicable. +/// +/// # Arguments +/// * `pool` - The database connection pool used to query and update user data. +/// * `event` - The unwrapped gift event containing the sender's information. +/// * `msg` - The message containing action details and trade index information. +async fn check_trade_index(pool: &Pool, event: &UnwrappedGift, msg: &Message) { + let message_kind = msg.get_inner_message_kind(); + if let Action::NewOrder | Action::TakeBuy | Action::TakeSell = message_kind.action { + match is_user_present(pool, event.sender.to_string()).await { + Ok(mut user) => { + if let (true, index) = message_kind.has_trade_index() { + let (_, sig): (Message, nostr_sdk::secp256k1::schnorr::Signature) = + serde_json::from_str(&event.rumor.content).unwrap(); + if index > user.last_trade_index + && msg + .get_inner_message_kind() + .verify_signature(event.sender, sig) + { + user.last_trade_index = index; + if user.update(pool).await.is_ok() { + tracing::info!("Update user trade index"); + } + } else { + tracing::info!("Invalid signature or trade index"); + send_cant_do_msg( + None, + msg.get_inner_message_kind().id, + Some(CantDoReason::InvalidTradeIndex), + &event.rumor.pubkey, + ) + .await; + } + } + } + Err(_) => { + if let (true, last_trade_index) = message_kind.has_trade_index() { + let new_user = User { + pubkey: event.sender.to_string(), + last_trade_index, + ..Default::default() + }; + if new_user.create(pool).await.is_ok() { + tracing::info!("Added new user for rate"); + } + } + } + } + } +} + /// Handles the processing of a single message action by routing it to the appropriate handler /// based on the action type. This is the core message routing logic of the application. /// @@ -145,32 +206,33 @@ pub async fn run( if event.rumor.created_at.as_u64() < since_time { continue; } + let (message, sig): (Message, Signature) = + serde_json::from_str(&event.rumor.content).unwrap(); + let inner_message = message.get_inner_message_kind(); + if !inner_message.verify_signature(event.rumor.pubkey, sig) { + tracing::warn!("Error in event verification"); + continue; + } - // Parse and process the message - let message = Message::from_json(&event.rumor.content); - match message { - Ok(msg) => { - if msg.get_inner_message_kind().verify() { - if let Some(action) = msg.inner_action() { - if let Err(e) = handle_message_action( - &action, - msg, - &event, - &my_keys, - &pool, - ln_client, - rate_list.clone(), - ) - .await - { - warning_msg(&action, e) - } - } + // Check if message is message with trade index + check_trade_index(&pool, &event, &message).await; + + if inner_message.verify() { + if let Some(action) = message.inner_action() { + if let Err(e) = handle_message_action( + &action, + message, + &event, + &my_keys, + &pool, + ln_client, + rate_list.clone(), + ) + .await + { + warning_msg(&action, e) } } - Err(e) => { - tracing::warn!("Failed to parse event message from JSON: {:?}", e) - } } } } diff --git a/src/app/add_invoice.rs b/src/app/add_invoice.rs index f928bd27..b2ba221b 100644 --- a/src/app/add_invoice.rs +++ b/src/app/add_invoice.rs @@ -3,7 +3,7 @@ use crate::util::{send_cant_do_msg, send_new_order_msg, show_hold_invoice, updat use anyhow::{Error, Result}; -use mostro_core::message::{Action, Content, Message}; +use mostro_core::message::{Action, CantDoReason, Message, Payload}; use mostro_core::order::SmallOrder; use mostro_core::order::{Kind, Order, Status}; use nostr::nips::nip59::UnwrappedGift; @@ -57,8 +57,14 @@ pub async fn add_invoice_action( } }; // Only the buyer can add an invoice - if buyer_pubkey != event.sender { - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + if buyer_pubkey != event.rumor.pubkey { + send_cant_do_msg( + request_id, + Some(order.id), + Some(CantDoReason::InvalidPeer), + &event.rumor.pubkey, + ) + .await; return Ok(()); } @@ -82,7 +88,8 @@ pub async fn add_invoice_action( Some(order.id), Action::IncorrectInvoiceAmount, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); @@ -107,6 +114,7 @@ pub async fn add_invoice_action( Action::InvoiceUpdated, None, &buyer_pubkey, + None, ) .await; return Ok(()); @@ -117,7 +125,8 @@ pub async fn add_invoice_action( Some(order.id), Action::NotAllowedByStatus, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); @@ -161,8 +170,9 @@ pub async fn add_invoice_action( None, Some(order.id), Action::BuyerTookOrder, - Some(Content::Order(order_data.clone())), + Some(Payload::Order(order_data.clone())), &seller_pubkey, + None, ) .await; // We send a message to buyer saying seller paid @@ -170,8 +180,9 @@ pub async fn add_invoice_action( request_id, Some(order.id), Action::HoldInvoicePaymentAccepted, - Some(Content::Order(order_data)), + Some(Payload::Order(order_data)), &buyer_pubkey, + None, ) .await; } else { diff --git a/src/app/admin_add_solver.rs b/src/app/admin_add_solver.rs index 983e1302..22e7d1e6 100644 --- a/src/app/admin_add_solver.rs +++ b/src/app/admin_add_solver.rs @@ -1,7 +1,7 @@ use crate::util::{send_cant_do_msg, send_dm}; use anyhow::Result; -use mostro_core::message::{Action, Content, Message}; +use mostro_core::message::{Action, Message, Payload}; use mostro_core::user::User; use nostr::nips::nip59::UnwrappedGift; use nostr_sdk::prelude::*; @@ -19,13 +19,13 @@ pub async fn admin_add_solver_action( let request_id = msg.get_inner_message_kind().request_id; let inner_message = msg.get_inner_message_kind(); - let content = if let Some(content) = &inner_message.content { - content + let payload = if let Some(payload) = &inner_message.payload { + payload } else { error!("No pubkey found!"); return Ok(()); }; - let npubkey = if let Content::TextMessage(p) = content { + let npubkey = if let Payload::TextMessage(p) = payload { p } else { error!("No pubkey found!"); @@ -33,24 +33,25 @@ pub async fn admin_add_solver_action( }; // Check if the pubkey is Mostro - if event.sender.to_string() != my_keys.public_key().to_string() { + if event.rumor.pubkey.to_string() != my_keys.public_key().to_string() { // We create a Message - send_cant_do_msg(request_id, None, None, &event.sender).await; + send_cant_do_msg(request_id, None, None, &event.rumor.pubkey).await; return Ok(()); } + let trade_index = inner_message.trade_index.unwrap_or(0); let public_key = PublicKey::from_bech32(npubkey)?.to_hex(); - let user = User::new(public_key, 0, 1, 0, 0); + let user = User::new(public_key, 0, 1, 0, 0, trade_index); // Use CRUD to create user match user.create(pool).await { Ok(r) => info!("Solver added: {:#?}", r), Err(ee) => error!("Error creating solver: {:#?}", ee), } // We create a Message for admin - let message = Message::new_dispute(request_id, None, Action::AdminAddSolver, None); + let message = Message::new_dispute(None, request_id, None, Action::AdminAddSolver, None); let message = message.as_json()?; // Send the message let sender_keys = crate::util::get_keys().unwrap(); - send_dm(&event.sender, sender_keys, message).await?; + send_dm(&event.rumor.pubkey, sender_keys, message).await?; Ok(()) } diff --git a/src/app/admin_cancel.rs b/src/app/admin_cancel.rs index 1c4d75ca..3bf9bc7d 100644 --- a/src/app/admin_cancel.rs +++ b/src/app/admin_cancel.rs @@ -31,15 +31,17 @@ pub async fn admin_cancel_action( } else { return Err(Error::msg("No order id")); }; + let inner_message = msg.get_inner_message_kind(); - match is_assigned_solver(pool, &event.sender.to_string(), order_id).await { + match is_assigned_solver(pool, &event.rumor.pubkey.to_string(), order_id).await { Ok(false) => { send_new_order_msg( - msg.get_inner_message_kind().request_id, + inner_message.request_id, Some(order_id), Action::IsNotYourDispute, None, - &event.sender, + &event.rumor.pubkey, + inner_message.trade_index, ) .await; return Ok(()); @@ -62,25 +64,27 @@ pub async fn admin_cancel_action( // Was order cooperatively cancelled? if order.status == Status::CooperativelyCanceled.to_string() { let message = MessageKind::new( - request_id, Some(order_id), + request_id, + inner_message.trade_index, Action::CooperativeCancelAccepted, None, ); if let Ok(message) = message.as_json() { let sender_keys = crate::util::get_keys().unwrap(); - let _ = send_dm(&event.sender, sender_keys, message).await; + let _ = send_dm(&event.rumor.pubkey, sender_keys, message).await; } return Ok(()); } if order.status != Status::Dispute.to_string() { send_new_order_msg( - msg.get_inner_message_kind().request_id, + inner_message.request_id, Some(order.id), Action::NotAllowedByStatus, None, - &event.sender, + &event.rumor.pubkey, + inner_message.trade_index, ) .await; return Ok(()); @@ -135,11 +139,17 @@ pub async fn admin_cancel_action( let order_updated = update_order_event(my_keys, Status::CanceledByAdmin, &order).await?; order_updated.update(pool).await?; // We create a Message for cancel - let message = Message::new_order(request_id, Some(order.id), Action::AdminCanceled, None); + let message = Message::new_order( + Some(order.id), + request_id, + inner_message.trade_index, + Action::AdminCanceled, + None, + ); let message = message.as_json()?; // Message to admin let sender_keys = crate::util::get_keys().unwrap(); - send_dm(&event.sender, sender_keys, message.clone()).await?; + send_dm(&event.rumor.pubkey, sender_keys, message.clone()).await?; let (seller_pubkey, buyer_pubkey) = match (&order.seller_pubkey, &order.buyer_pubkey) { (Some(seller), Some(buyer)) => ( diff --git a/src/app/admin_settle.rs b/src/app/admin_settle.rs index c5358a89..c25a8626 100644 --- a/src/app/admin_settle.rs +++ b/src/app/admin_settle.rs @@ -33,15 +33,17 @@ pub async fn admin_settle_action( } else { return Err(Error::msg("No order id")); }; + let inner_message = msg.get_inner_message_kind(); - match is_assigned_solver(pool, &event.sender.to_string(), order_id).await { + match is_assigned_solver(pool, &event.rumor.pubkey.to_string(), order_id).await { Ok(false) => { send_new_order_msg( msg.get_inner_message_kind().request_id, Some(order_id), Action::IsNotYourDispute, None, - &event.sender, + &event.rumor.pubkey, + inner_message.trade_index, ) .await; return Ok(()); @@ -64,25 +66,27 @@ pub async fn admin_settle_action( // Was orde cooperatively cancelled? if order.status == Status::CooperativelyCanceled.to_string() { let message = MessageKind::new( - msg.get_inner_message_kind().request_id, Some(order_id), + msg.get_inner_message_kind().request_id, + inner_message.trade_index, Action::CooperativeCancelAccepted, None, ); if let Ok(message) = message.as_json() { let sender_keys = crate::util::get_keys().unwrap(); - let _ = send_dm(&event.sender, sender_keys, message).await; + let _ = send_dm(&event.rumor.pubkey, sender_keys, message).await; } return Ok(()); } if order.status != Status::Dispute.to_string() { send_new_order_msg( - msg.get_inner_message_kind().request_id, + inner_message.request_id, Some(order.id), Action::NotAllowedByStatus, None, - &event.sender, + &event.rumor.pubkey, + inner_message.trade_index, ) .await; return Ok(()); @@ -140,15 +144,16 @@ pub async fn admin_settle_action( } // We create a Message for settle let message = Message::new_order( - request_id, Some(order_updated.id), + request_id, + inner_message.trade_index, Action::AdminSettled, None, ); let message = message.as_json()?; // Message to admin let sender_keys = crate::util::get_keys().unwrap(); - send_dm(&event.sender, sender_keys.clone(), message.clone()).await?; + send_dm(&event.rumor.pubkey, sender_keys.clone(), message.clone()).await?; if let Some(ref seller_pubkey) = order_updated.seller_pubkey { send_dm( &PublicKey::from_str(seller_pubkey)?, diff --git a/src/app/admin_take_dispute.rs b/src/app/admin_take_dispute.rs index 072b91f8..4c94093e 100644 --- a/src/app/admin_take_dispute.rs +++ b/src/app/admin_take_dispute.rs @@ -4,7 +4,7 @@ use crate::util::{get_nostr_client, send_cant_do_msg, send_dm, send_new_order_ms use anyhow::{Error, Result}; use mostro_core::dispute::{Dispute, Status}; -use mostro_core::message::{Action, Content, Message, Peer}; +use mostro_core::message::{Action, Message, Payload, Peer}; use mostro_core::order::Order; use nostr::nips::nip59::UnwrappedGift; use nostr_sdk::prelude::*; @@ -62,7 +62,8 @@ pub async fn admin_take_dispute_action( Some(dispute_id), Action::NotFound, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); @@ -71,9 +72,9 @@ pub async fn admin_take_dispute_action( // Check if the pubkey is a solver or admin if let Ok(dispute_status) = Status::from_str(&dispute.status) { - if !pubkey_event_can_solve(pool, &event.sender, dispute_status).await { + if !pubkey_event_can_solve(pool, &event.rumor.pubkey, dispute_status).await { // We create a Message - send_cant_do_msg(request_id, Some(dispute_id), None, &event.sender).await; + send_cant_do_msg(request_id, Some(dispute_id), None, &event.rumor.pubkey).await; return Ok(()); } } else { @@ -95,10 +96,10 @@ pub async fn admin_take_dispute_action( // Update dispute fields dispute.status = Status::InProgress.to_string(); - dispute.solver_pubkey = Some(event.sender.to_string()); + dispute.solver_pubkey = Some(event.rumor.pubkey.to_string()); dispute.taken_at = Timestamp::now().as_u64() as i64; - info!("Dispute {} taken by {}", dispute_id, event.sender); + info!("Dispute {} taken by {}", dispute_id, event.rumor.pubkey); // Assign token for admin message new_order.seller_token = dispute.seller_token; new_order.buyer_token = dispute.buyer_token; @@ -107,29 +108,32 @@ pub async fn admin_take_dispute_action( // We create a Message for admin let message = Message::new_dispute( - request_id, Some(dispute_id), + request_id, + None, Action::AdminTookDispute, - Some(Content::Order(new_order)), + Some(Payload::Order(new_order)), ); let message = message.as_json()?; let sender_keys = crate::util::get_keys().unwrap(); - send_dm(&event.sender, sender_keys, message).await?; + send_dm(&event.rumor.pubkey, sender_keys, message).await?; // Now we create a message to both parties of the order // to them know who will assist them on the dispute - let solver_pubkey = Peer::new(event.sender.to_hex()); + let solver_pubkey = Peer::new(event.rumor.pubkey.to_hex()); let msg_to_buyer = Message::new_order( - None, Some(order.id), + request_id, + None, Action::AdminTookDispute, - Some(Content::Peer(solver_pubkey.clone())), + Some(Payload::Peer(solver_pubkey.clone())), ); let msg_to_seller = Message::new_order( - None, Some(order.id), + request_id, + None, Action::AdminTookDispute, - Some(Content::Peer(solver_pubkey)), + Some(Payload::Peer(solver_pubkey)), ); let (seller_pubkey, buyer_pubkey) = match (&order.seller_pubkey, &order.buyer_pubkey) { diff --git a/src/app/cancel.rs b/src/app/cancel.rs index ab5dbc21..d66da4b6 100644 --- a/src/app/cancel.rs +++ b/src/app/cancel.rs @@ -30,7 +30,7 @@ pub async fn cancel_action( } else { return Err(Error::msg("No order id")); }; - let user_pubkey = event.sender.to_string(); + let user_pubkey = event.rumor.pubkey.to_string(); let mut order = match find_order_by_id(pool, order_id, &user_pubkey).await { Ok(order) => order, @@ -49,7 +49,8 @@ pub async fn cancel_action( Some(order.id), Action::IsNotYourOrder, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; } else { @@ -64,7 +65,8 @@ pub async fn cancel_action( Some(order.id), Action::Canceled, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; } @@ -108,7 +110,7 @@ pub async fn cancel_action( Some(ref initiator_pubkey) => { if initiator_pubkey == &user_pubkey { // We create a Message - send_cant_do_msg(request_id, Some(order_id), None, &event.sender).await; + send_cant_do_msg(request_id, Some(order_id), None, &event.rumor.pubkey).await; return Ok(()); } else { if let Some(hash) = &order.hash { @@ -131,7 +133,8 @@ pub async fn cancel_action( Some(order.id), Action::CooperativeCancelAccepted, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; let counterparty_pubkey = PublicKey::from_str(&counterparty_pubkey)?; @@ -141,6 +144,7 @@ pub async fn cancel_action( Action::CooperativeCancelAccepted, None, &counterparty_pubkey, + None, ) .await; info!("Cancel: Order Id {order_id} canceled cooperatively!"); @@ -156,7 +160,8 @@ pub async fn cancel_action( Some(order.id), Action::CooperativeCancelInitiatedByYou, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; let counterparty_pubkey = PublicKey::from_str(&counterparty_pubkey)?; @@ -166,6 +171,7 @@ pub async fn cancel_action( Action::CooperativeCancelInitiatedByPeer, None, &counterparty_pubkey, + None, ) .await; } @@ -187,7 +193,7 @@ pub async fn cancel_add_invoice( info!("Order Id {}: Funds returned to seller", &order.id); } - let user_pubkey = event.sender.to_string(); + let user_pubkey = event.rumor.pubkey.to_string(); let (seller_pubkey, buyer_pubkey) = match (&order.seller_pubkey, &order.buyer_pubkey) { (Some(seller), Some(buyer)) => (PublicKey::from_str(seller.as_str())?, buyer), @@ -197,7 +203,7 @@ pub async fn cancel_add_invoice( if buyer_pubkey != &user_pubkey { // We create a Message - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + send_cant_do_msg(request_id, Some(order.id), None, &event.rumor.pubkey).await; return Ok(()); } @@ -211,10 +217,19 @@ pub async fn cancel_add_invoice( Some(order.id), Action::Canceled, None, - &event.sender, + &event.rumor.pubkey, + None, + ) + .await; + send_new_order_msg( + None, + Some(order.id), + Action::Canceled, + None, + &seller_pubkey, + None, ) .await; - send_new_order_msg(None, Some(order.id), Action::Canceled, None, &seller_pubkey).await; Ok(()) } else { // We re-publish the event with Pending status @@ -249,7 +264,7 @@ pub async fn cancel_pay_hold_invoice( info!("Order Id {}: Funds returned to seller", &order.id); } } - let user_pubkey = event.sender.to_string(); + let user_pubkey = event.rumor.pubkey.to_string(); let (seller_pubkey, buyer_pubkey) = match (&order.seller_pubkey, &order.buyer_pubkey) { (Some(seller), Some(buyer)) => (PublicKey::from_str(seller.as_str())?, buyer), @@ -259,7 +274,7 @@ pub async fn cancel_pay_hold_invoice( if seller_pubkey.to_string() != user_pubkey { // We create a Message - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + send_cant_do_msg(request_id, Some(order.id), None, &event.rumor.pubkey).await; return Ok(()); } @@ -273,10 +288,19 @@ pub async fn cancel_pay_hold_invoice( Some(order.id), Action::Canceled, None, - &event.sender, + &event.rumor.pubkey, + None, + ) + .await; + send_new_order_msg( + None, + Some(order.id), + Action::Canceled, + None, + &seller_pubkey, + None, ) .await; - send_new_order_msg(None, Some(order.id), Action::Canceled, None, &seller_pubkey).await; Ok(()) } else { // We re-publish the event with Pending status diff --git a/src/app/dispute.rs b/src/app/dispute.rs index 503a9917..855c11eb 100644 --- a/src/app/dispute.rs +++ b/src/app/dispute.rs @@ -11,7 +11,7 @@ use crate::util::{get_nostr_client, send_cant_do_msg, send_new_order_msg}; use anyhow::{Error, Result}; use mostro_core::dispute::Dispute; -use mostro_core::message::{Action, Content, Message}; +use mostro_core::message::{Action, Message, Payload}; use mostro_core::order::{Order, Status}; use nostr::nips::nip59::UnwrappedGift; use nostr_sdk::prelude::*; @@ -120,7 +120,8 @@ async fn get_valid_order( Some(order.id), Action::NotAllowedByStatus, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Err(Error::msg(format!( @@ -178,12 +179,12 @@ pub async fn dispute_action( (_, None) => return Err(Error::msg("Missing buyer pubkey")), }; - let message_sender = event.sender.to_string(); + let message_sender = event.rumor.pubkey.to_string(); let (counterpart, is_buyer_dispute) = match get_counterpart_info(&message_sender, &buyer, &seller) { Ok((counterpart, is_buyer_dispute)) => (counterpart, is_buyer_dispute), Err(_) => { - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + send_cant_do_msg(request_id, Some(order.id), None, &event.rumor.pubkey).await; return Ok(()); } }; @@ -239,8 +240,9 @@ pub async fn dispute_action( msg.get_inner_message_kind().request_id, Some(order_id), Action::DisputeInitiatedByYou, - Some(Content::Dispute(dispute.clone().id, initiator_token)), + Some(Payload::Dispute(dispute.clone().id, initiator_token)), &initiator_pubkey, + None, ) .await; @@ -256,8 +258,9 @@ pub async fn dispute_action( msg.get_inner_message_kind().request_id, Some(order_id), Action::DisputeInitiatedByPeer, - Some(Content::Dispute(dispute.clone().id, counterpart_token)), + Some(Payload::Dispute(dispute.clone().id, counterpart_token)), &counterpart_pubkey, + None, ) .await; diff --git a/src/app/fiat_sent.rs b/src/app/fiat_sent.rs index 49e418b1..aeaae559 100644 --- a/src/app/fiat_sent.rs +++ b/src/app/fiat_sent.rs @@ -1,7 +1,7 @@ use crate::util::{send_cant_do_msg, send_new_order_msg, update_order_event}; use anyhow::{Error, Result}; -use mostro_core::message::{Action, Content, Message, Peer}; +use mostro_core::message::{Action, Message, Payload, Peer}; use mostro_core::order::{Order, Status}; use nostr::nips::nip59::UnwrappedGift; use nostr_sdk::prelude::*; @@ -38,14 +38,15 @@ pub async fn fiat_sent_action( Some(order.id), Action::NotAllowedByStatus, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); } // Check if the pubkey is the buyer - if Some(event.sender.to_string()) != order.buyer_pubkey { - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + if Some(event.rumor.pubkey.to_string()) != order.buyer_pubkey { + send_cant_do_msg(request_id, Some(order.id), None, &event.rumor.pubkey).await; return Ok(()); } @@ -62,15 +63,16 @@ pub async fn fiat_sent_action( return Ok(()); } }; - let peer = Peer::new(event.sender.to_string()); + let peer = Peer::new(event.rumor.pubkey.to_string()); // We a message to the seller send_new_order_msg( None, Some(order.id), Action::FiatSentOk, - Some(Content::Peer(peer)), + Some(Payload::Peer(peer)), &seller_pubkey, + None, ) .await; // We send a message to buyer to wait @@ -80,8 +82,9 @@ pub async fn fiat_sent_action( msg.get_inner_message_kind().request_id, Some(order.id), Action::FiatSentOk, - Some(Content::Peer(peer)), - &event.sender, + Some(Payload::Peer(peer)), + &event.rumor.pubkey, + None, ) .await; diff --git a/src/app/order.rs b/src/app/order.rs index b256ca9c..901fe362 100644 --- a/src/app/order.rs +++ b/src/app/order.rs @@ -33,7 +33,8 @@ pub async fn order_action( order.id, Action::IncorrectInvoiceAmount, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); @@ -48,7 +49,7 @@ pub async fn order_action( // in case of single order do like usual if let (Some(min), Some(max)) = (order.min_amount, order.max_amount) { if min >= max { - send_cant_do_msg(request_id, order.id, None, &event.sender).await; + send_cant_do_msg(request_id, order.id, None, &event.rumor.pubkey).await; return Ok(()); } if order.amount != 0 { @@ -57,7 +58,8 @@ pub async fn order_action( None, Action::InvalidSatsAmount, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); @@ -89,7 +91,8 @@ pub async fn order_action( None, Action::InvalidSatsAmount, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); @@ -103,7 +106,8 @@ pub async fn order_action( None, Action::OutOfRangeSatsAmount, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); @@ -114,9 +118,11 @@ pub async fn order_action( pool, my_keys, order, - &event.sender.to_string(), + event.rumor.pubkey, event.sender, + event.rumor.pubkey, request_id, + msg.get_inner_message_kind().trade_index, ) .await?; } diff --git a/src/app/rate_user.rs b/src/app/rate_user.rs index f25ad707..52e63bfe 100644 --- a/src/app/rate_user.rs +++ b/src/app/rate_user.rs @@ -2,7 +2,7 @@ use crate::util::{send_cant_do_msg, send_new_order_msg, update_user_rating_event use crate::NOSTR_CLIENT; use anyhow::{Error, Result}; -use mostro_core::message::{Action, Content, Message}; +use mostro_core::message::{Action, Message, Payload}; use mostro_core::order::{Order, Status}; use mostro_core::rating::Rating; use mostro_core::NOSTR_REPLACEABLE_EVENT_KIND; @@ -76,10 +76,10 @@ pub async fn update_user_reputation_action( (_, None) => return Err(Error::msg("Missing buyer pubkey")), }; - let message_sender = event.sender.to_string(); + let message_sender = event.rumor.pubkey.to_string(); if order.status != Status::Success.to_string() { - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + send_cant_do_msg(request_id, Some(order.id), None, &event.rumor.pubkey).await; error!("Order Id {order_id} wrong status"); return Ok(()); } @@ -100,7 +100,7 @@ pub async fn update_user_reputation_action( // Add a check in case of no counterpart found if counterpart.is_empty() { // We create a Message - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + send_cant_do_msg(request_id, Some(order.id), None, &event.rumor.pubkey).await; return Ok(()); }; @@ -120,7 +120,13 @@ pub async fn update_user_reputation_action( // Check if content of Peer is the same of counterpart let rating; - if let Some(Content::RatingUser(v)) = msg.get_inner_message_kind().content.to_owned() { + if let Some(Payload::RatingUser(v)) = msg.get_inner_message_kind().payload.to_owned() { + if !(MIN_RATING..=MAX_RATING).contains(&v) { + return Err(Error::msg(format!( + "Rating must be between {} and {}", + MIN_RATING, MAX_RATING + ))); + } rating = v; } else { return Err(Error::msg("No rating present")); @@ -171,8 +177,9 @@ pub async fn update_user_reputation_action( msg.get_inner_message_kind().request_id, Some(order.id), Action::RateReceived, - Some(Content::RatingUser(rating)), - &event.sender, + Some(Payload::RatingUser(rating)), + &event.rumor.pubkey, + None, ) .await; } diff --git a/src/app/release.rs b/src/app/release.rs index 52cab525..4acdc7ab 100644 --- a/src/app/release.rs +++ b/src/app/release.rs @@ -11,7 +11,7 @@ use crate::NOSTR_CLIENT; use anyhow::{Error, Result}; use fedimint_tonic_lnd::lnrpc::payment::PaymentStatus; use lnurl::lightning_address::LightningAddress; -use mostro_core::message::{Action, Message}; +use mostro_core::message::{Action, CantDoReason, Message}; use mostro_core::order::{Order, Status}; use nostr::nips::nip59::UnwrappedGift; use nostr_sdk::prelude::*; @@ -50,6 +50,7 @@ pub async fn check_failure_retries(order: &Order, request_id: Option) -> Re Action::PaymentFailed, None, &buyer_pubkey, + None, ) .await; @@ -89,7 +90,7 @@ pub async fn release_action( return Ok(()); } }; - let seller_pubkey = event.sender; + let seller_pubkey = event.rumor.pubkey; let current_status = if let Ok(current_status) = Status::from_str(&order.status) { current_status @@ -106,14 +107,21 @@ pub async fn release_action( Some(order.id), Action::NotAllowedByStatus, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); } if &seller_pubkey.to_string() != seller_pubkey_hex { - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + send_cant_do_msg( + request_id, + Some(order.id), + Some(CantDoReason::InvalidPeer), + &event.rumor.pubkey, + ) + .await; return Ok(()); } @@ -137,6 +145,7 @@ pub async fn release_action( Action::HoldInvoicePaymentSettled, None, &seller_pubkey, + None, ) .await; @@ -145,7 +154,15 @@ pub async fn release_action( Some(buyer) => PublicKey::from_str(buyer.as_str())?, _ => return Err(Error::msg("Missing buyer pubkeys")), }; - send_new_order_msg(None, Some(order_id), Action::Released, None, &buyer_pubkey).await; + send_new_order_msg( + None, + Some(order_id), + Action::Released, + None, + &buyer_pubkey, + None, + ) + .await; let _ = do_payment(order_updated, request_id).await; @@ -252,6 +269,7 @@ async fn payment_success( Action::PurchaseCompleted, None, buyer_pubkey, + None, ) .await; @@ -271,9 +289,11 @@ async fn payment_success( if new_order.kind == "sell" { new_order.buyer_pubkey = None; new_order.master_buyer_pubkey = None; + new_order.trade_index_buyer = None; } else { new_order.seller_pubkey = None; new_order.master_seller_pubkey = None; + new_order.trade_index_seller = None; } if let Some(min_amount) = &order.min_amount { match new_max.cmp(min_amount) { @@ -346,8 +366,20 @@ async fn payment_success( Ordering::Less => {} } } else { - send_cant_do_msg(None, Some(order.id), None, buyer_pubkey).await; - send_cant_do_msg(request_id, Some(order.id), None, seller_pubkey).await; + send_cant_do_msg( + None, + Some(order.id), + Some(CantDoReason::InvalidAmount), + buyer_pubkey, + ) + .await; + send_cant_do_msg( + request_id, + Some(order.id), + Some(CantDoReason::InvalidAmount), + seller_pubkey, + ) + .await; } } } diff --git a/src/app/take_buy.rs b/src/app/take_buy.rs index 88ae886c..05938210 100644 --- a/src/app/take_buy.rs +++ b/src/app/take_buy.rs @@ -37,8 +37,8 @@ pub async fn take_buy_action( }; // Maker can't take own order - if order.kind != Kind::Buy.to_string() || order.creator_pubkey == event.sender.to_hex() { - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + if order.kind != Kind::Buy.to_string() || order.creator_pubkey == event.rumor.pubkey.to_hex() { + send_cant_do_msg(request_id, Some(order.id), None, &event.rumor.pubkey).await; return Ok(()); } @@ -58,7 +58,7 @@ pub async fn take_buy_action( }; // We update the pubkey - let seller_pubkey = event.sender; + let seller_pubkey = event.rumor.pubkey; // Seller can take pending orders only match order_status { Status::Pending => {} @@ -69,6 +69,7 @@ pub async fn take_buy_action( Action::NotAllowedByStatus, None, &seller_pubkey, + None, ) .await; return Ok(()); @@ -84,7 +85,8 @@ pub async fn take_buy_action( Some(order.id), Action::OutOfRangeFiatAmount, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); @@ -99,6 +101,8 @@ pub async fn take_buy_action( order.fee = fee; } + // Update trade index for seller + order.trade_index_seller = msg.get_inner_message_kind().trade_index; // Timestamp order take time order.taken_at = Timestamp::now().as_u64() as i64; diff --git a/src/app/take_sell.rs b/src/app/take_sell.rs index d9afd3b3..9dc5fbc9 100644 --- a/src/app/take_sell.rs +++ b/src/app/take_sell.rs @@ -5,7 +5,7 @@ use crate::util::{ }; use anyhow::{Error, Result}; -use mostro_core::message::{Action, Message}; +use mostro_core::message::{Action, CantDoReason, Message}; use mostro_core::order::{Kind, Order, Status}; use nostr::nips::nip59::UnwrappedGift; use nostr_sdk::prelude::*; @@ -38,8 +38,8 @@ pub async fn take_sell_action( }; // Maker can't take own order - if order.creator_pubkey == event.sender.to_hex() { - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + if order.creator_pubkey == event.rumor.pubkey.to_hex() { + send_cant_do_msg(request_id, Some(order.id), None, &event.rumor.pubkey).await; return Ok(()); } @@ -47,7 +47,8 @@ pub async fn take_sell_action( return Ok(()); } - let buyer_pubkey = event.sender; + // Get trade pubkey of the buyer + let buyer_trade_pubkey = event.rumor.pubkey; let seller_pubkey = match &order.seller_pubkey { Some(seller) => PublicKey::from_str(seller.as_str())?, @@ -72,8 +73,8 @@ pub async fn take_sell_action( send_cant_do_msg( request_id, Some(order.id), - Some(e.to_string()), - &event.sender, + Some(CantDoReason::InvalidInvoice), + &event.rumor.pubkey, ) .await; error!("{e}"); @@ -100,7 +101,8 @@ pub async fn take_sell_action( Some(order.id), Action::NotAllowedByStatus, None, - &buyer_pubkey, + &buyer_trade_pubkey, + None, ) .await; return Ok(()); @@ -116,14 +118,16 @@ pub async fn take_sell_action( Some(order.id), Action::OutOfRangeFiatAmount, None, - &event.sender, + &event.rumor.pubkey, + None, ) .await; return Ok(()); } // Add buyer pubkey to order - order.buyer_pubkey = Some(buyer_pubkey.to_string()); + order.buyer_pubkey = Some(buyer_trade_pubkey.to_string()); + order.trade_index_buyer = msg.get_inner_message_kind().trade_index; // Timestamp take order time order.taken_at = Timestamp::now().as_u64() as i64; @@ -137,7 +141,7 @@ pub async fn take_sell_action( } if pr.is_none() { - match set_waiting_invoice_status(&mut order, buyer_pubkey, request_id).await { + match set_waiting_invoice_status(&mut order, buyer_trade_pubkey, request_id).await { Ok(_) => { // Update order status if let Ok(order_updated) = @@ -156,7 +160,7 @@ pub async fn take_sell_action( show_hold_invoice( my_keys, pr, - &buyer_pubkey, + &buyer_trade_pubkey, &seller_pubkey, order, request_id, diff --git a/src/db.rs b/src/db.rs index 7cf498c8..1485b396 100644 --- a/src/db.rs +++ b/src/db.rs @@ -301,6 +301,7 @@ pub async fn find_solver_pubkey(pool: &SqlitePool, solver_npub: String) -> anyho SELECT * FROM users WHERE pubkey == ?1 AND is_solver == true + LIMIT 1 "#, ) .bind(solver_npub) @@ -310,6 +311,22 @@ pub async fn find_solver_pubkey(pool: &SqlitePool, solver_npub: String) -> anyho Ok(user) } +pub async fn is_user_present(pool: &SqlitePool, public_key: String) -> anyhow::Result { + let user = sqlx::query_as::<_, User>( + r#" + SELECT * + FROM users + WHERE pubkey == ?1 + LIMIT 1 + "#, + ) + .bind(public_key) + .fetch_one(pool) + .await?; + + Ok(user) +} + pub async fn is_assigned_solver( pool: &SqlitePool, solver_pubkey: &str, diff --git a/src/error.rs b/src/error.rs index 0d3190e8..fcce1f40 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,7 @@ pub enum MostroError { LnAddressWrongAmount, LnPaymentError(String), LnNodeError(String), + InvalidOrderKind, } impl std::error::Error for MostroError {} @@ -37,6 +38,7 @@ impl fmt::Display for MostroError { MostroError::LnAddressParseError => write!(f, "Ln address parsing error - please check your address"), MostroError::LnPaymentError(e) => write!(f, "Lightning payment failure cause: {}",e), MostroError::LnNodeError(e) => write!(f, "Lightning node connection failure caused by: {}",e), + MostroError::InvalidOrderKind => write!(f, "Invalid order kind"), } } } diff --git a/src/flow.rs b/src/flow.rs index 3b540ee0..02d63cb2 100644 --- a/src/flow.rs +++ b/src/flow.rs @@ -1,6 +1,6 @@ use crate::util::send_new_order_msg; use anyhow::{Error, Result}; -use mostro_core::message::{Action, Content}; +use mostro_core::message::{Action, Payload}; use mostro_core::order::{Kind, SmallOrder, Status}; use nostr_sdk::prelude::*; use sqlx_crud::Crud; @@ -64,8 +64,9 @@ pub async fn hold_invoice_paid(hash: &str, request_id: Option) -> Result<() request_id, Some(order.id), Action::BuyerTookOrder, - Some(Content::Order(order_data.clone())), + Some(Payload::Order(order_data.clone())), &seller_pubkey, + None, ) .await; // We send a message to buyer saying seller paid @@ -73,8 +74,9 @@ pub async fn hold_invoice_paid(hash: &str, request_id: Option) -> Result<() request_id, Some(order.id), Action::HoldInvoicePaymentAccepted, - Some(Content::Order(order_data)), + Some(Payload::Order(order_data)), &buyer_pubkey, + None, ) .await; } else { @@ -89,8 +91,9 @@ pub async fn hold_invoice_paid(hash: &str, request_id: Option) -> Result<() request_id, Some(order.id), Action::AddInvoice, - Some(Content::Order(order_data)), + Some(Payload::Order(order_data)), &buyer_pubkey, + None, ) .await; @@ -101,6 +104,7 @@ pub async fn hold_invoice_paid(hash: &str, request_id: Option) -> Result<() Action::WaitingBuyerInvoice, None, &seller_pubkey, + None, ) .await; } diff --git a/src/main.rs b/src/main.rs index 944c9733..49ddd707 100644 --- a/src/main.rs +++ b/src/main.rs @@ -110,7 +110,7 @@ mod tests { #[test] fn test_message_deserialize_serialize() { - let sample_message = r#"{"order":{"version":1,"request_id":1,"id":"7dd204d2-d06c-4406-a3d9-4415f4a8b9c9","action":"fiat-sent","content":null}}"#; + let sample_message = r#"{"order":{"version":1,"request_id":1,"trade_index":null,"id":"7dd204d2-d06c-4406-a3d9-4415f4a8b9c9","action":"fiat-sent","payload":null}}"#; let message = Message::from_json(sample_message).unwrap(); assert!(message.verify()); let json_message = message.as_json().unwrap(); @@ -119,7 +119,7 @@ mod tests { #[test] fn test_wrong_message_should_fail() { - let sample_message = r#"{"order":{"version":1,"request_id":1,"action":"take-sell","content":{"order":{"kind":"sell","status":"pending","amount":100,"fiat_code":"XXX","fiat_amount":10,"payment_method":"SEPA","premium":1,"payment_request":null,"created_at":1640839235}}}}"#; + let sample_message = r#"{"order":{"version":1,"request_id":1,"action":"take-sell","payload":{"order":{"kind":"sell","status":"pending","amount":100,"fiat_code":"XXX","fiat_amount":10,"payment_method":"SEPA","premium":1,"payment_request":null,"created_at":1640839235}}}}"#; let message = Message::from_json(sample_message).unwrap(); assert!(!message.verify()); } diff --git a/src/nip33.rs b/src/nip33.rs index e3bef7e9..1a668493 100644 --- a/src/nip33.rs +++ b/src/nip33.rs @@ -29,8 +29,10 @@ pub fn new_event( let mut tags: Vec = Vec::with_capacity(1 + extra_tags.len()); tags.push(Tag::identifier(identifier)); tags.extend(extra_tags); + let tags = Tags::new(tags); - EventBuilder::new(Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND), content, tags) + EventBuilder::new(Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND), content) + .tags(tags) .sign_with_keys(keys) } diff --git a/src/nip59.rs b/src/nip59.rs index d6b25e39..e92ee9f7 100644 --- a/src/nip59.rs +++ b/src/nip59.rs @@ -1,4 +1,5 @@ use base64::engine::{general_purpose, Engine}; +use mostro_core::message::Message; use nostr_sdk::event::builder::Error as BuilderError; use nostr_sdk::nostr::nips::nip44::v2::{decrypt_to_bytes, encrypt_to_bytes, ConversationKey}; use nostr_sdk::prelude::*; @@ -18,10 +19,27 @@ use nostr_sdk::prelude::*; pub fn gift_wrap( sender_keys: &Keys, receiver: PublicKey, - content: String, + payload: String, expiration: Option, ) -> Result { - let rumor: UnsignedEvent = EventBuilder::text_note(content, []).build(sender_keys.public_key()); + // We convert back the string to a message + let message = Message::from_json(&payload).map_err(|e| { + BuilderError::NIP59(nip59::Error::Event(nostr::event::Error::Json(format!( + "Failed to parse message: {}", + e + )))) + })?; + // We sign the message + let sig = message.get_inner_message_kind().sign(sender_keys); + // We compose the content + let content = (message, sig); + let content = serde_json::to_string(&content).map_err(|e| { + BuilderError::NIP59(nip59::Error::Event(nostr::event::Error::Json(format!( + "Failed to serialize content: {}", + e + )))) + })?; + let rumor: UnsignedEvent = EventBuilder::text_note(content).build(sender_keys.public_key()); let seal: Event = seal(sender_keys, &receiver, rumor)?.sign_with_keys(sender_keys)?; gift_wrap_from_seal(&receiver, &seal, expiration) @@ -39,7 +57,7 @@ pub fn seal( // Encode with base64 let b64decoded_content = general_purpose::STANDARD.encode(encrypted_content); // Compose builder - Ok(EventBuilder::new(Kind::Seal, b64decoded_content, []) + Ok(EventBuilder::new(Kind::Seal, b64decoded_content) .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK))) } @@ -60,11 +78,16 @@ pub fn gift_wrap_from_seal( if let Some(timestamp) = expiration { tags.push(Tag::expiration(timestamp)); } + let tags = Tags::new(tags); // Encode with base64 let b64decoded_content = general_purpose::STANDARD.encode(encrypted_content); - EventBuilder::new(Kind::GiftWrap, b64decoded_content, tags) + let event = EventBuilder::new(Kind::GiftWrap, b64decoded_content) + .tags(tags) .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK)) - .sign_with_keys(&ephemeral_keys) + .build(ephemeral_keys.public_key()) + .sign_with_keys(&ephemeral_keys)?; + + Ok(event) } pub fn unwrap_gift_wrap(keys: &Keys, gift_wrap: &Event) -> Result { diff --git a/src/scheduler.rs b/src/scheduler.rs index 9ba7a885..1e66e8e4 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -50,8 +50,8 @@ async fn job_relay_list() { } } - if let Ok(relay_ev) = EventBuilder::new(NostrKind::RelayList, "", relay_tags) - .sign_with_keys(&mostro_keys) + if let Ok(relay_ev) = + EventBuilder::new(NostrKind::RelayList, "").sign_with_keys(&mostro_keys) { if let Ok(client) = get_nostr_client() { let _ = client.send_event(relay_ev).await; diff --git a/src/util.rs b/src/util.rs index 16d938d7..081af31a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -14,7 +14,8 @@ use crate::NOSTR_CLIENT; use anyhow::{Context, Error, Result}; use chrono::Duration; -use mostro_core::message::{Action, Content, Message}; +use mostro_core::message::CantDoReason; +use mostro_core::message::{Action, Message, Payload}; use mostro_core::order::{Kind as OrderKind, Order, SmallOrder, Status}; use nostr::nips::nip59::UnwrappedGift; use nostr_sdk::prelude::*; @@ -156,58 +157,39 @@ pub fn get_expiration_date(expire: Option) -> i64 { expire_date } +#[allow(clippy::too_many_arguments)] pub async fn publish_order( pool: &SqlitePool, keys: &Keys, new_order: &SmallOrder, - initiator_pubkey: &str, - ack_pubkey: PublicKey, + initiator_pubkey: PublicKey, + identity_pubkey: PublicKey, + trade_pubkey: PublicKey, request_id: Option, + trade_index: Option, ) -> Result<()> { - let mut fee = 0; - if new_order.amount > 0 { - fee = get_fee(new_order.amount); - } - - // Get expiration time of the order - let expiry_date = get_expiration_date(new_order.expires_at); - // Prepare a new default order - let mut new_order_db = Order { - id: Uuid::new_v4(), - kind: OrderKind::Sell.to_string(), - status: Status::Pending.to_string(), - creator_pubkey: initiator_pubkey.to_string(), - payment_method: new_order.payment_method.clone(), - amount: new_order.amount, - fee, - fiat_code: new_order.fiat_code.clone(), - min_amount: new_order.min_amount, - max_amount: new_order.max_amount, - fiat_amount: new_order.fiat_amount, - premium: new_order.premium, - buyer_invoice: new_order.buyer_invoice.clone(), - created_at: Timestamp::now().as_u64() as i64, - expires_at: expiry_date, - ..Default::default() + let new_order_db = match prepare_new_order( + new_order, + initiator_pubkey, + trade_index, + identity_pubkey, + trade_pubkey, + ) + .await + { + Some(order) => order, + None => { + return Ok(()); + } }; - if new_order.kind == Some(OrderKind::Buy) { - new_order_db.kind = OrderKind::Buy.to_string(); - new_order_db.buyer_pubkey = Some(initiator_pubkey.to_string()); - } else { - new_order_db.seller_pubkey = Some(initiator_pubkey.to_string()); - } - - // Request price from API in case amount is 0 - new_order_db.price_from_api = new_order.amount == 0; - // CRUD order creation let mut order = new_order_db.clone().create(pool).await?; let order_id = order.id; info!("New order saved Id: {}", order_id); // Get user reputation - let reputation = get_user_reputation(initiator_pubkey, keys).await?; + let reputation = get_user_reputation(&initiator_pubkey.to_string(), keys).await?; // We transform the order fields to tags to use in the event let tags = order_to_tags(&new_order_db, reputation); // nip33 kind with order fields as tags and order id as identifier @@ -226,8 +208,9 @@ pub async fn publish_order( request_id, Some(order_id), Action::NewOrder, - Some(Content::Order(order)), - &ack_pubkey, + Some(Payload::Order(order)), + &trade_pubkey, + trade_index, ) .await; @@ -240,19 +223,92 @@ pub async fn publish_order( .map_err(|err| err.into()) } +async fn prepare_new_order( + new_order: &SmallOrder, + initiator_pubkey: PublicKey, + trade_index: Option, + identity_pubkey: PublicKey, + trade_pubkey: PublicKey, +) -> Option { + let mut fee = 0; + if new_order.amount > 0 { + fee = get_fee(new_order.amount); + } + + // Get expiration time of the order + let expiry_date = get_expiration_date(new_order.expires_at); + + // Prepare a new default order + let mut new_order_db = Order { + id: Uuid::new_v4(), + kind: OrderKind::Sell.to_string(), + status: Status::Pending.to_string(), + creator_pubkey: initiator_pubkey.to_string(), + payment_method: new_order.payment_method.clone(), + amount: new_order.amount, + fee, + fiat_code: new_order.fiat_code.clone(), + min_amount: new_order.min_amount, + max_amount: new_order.max_amount, + fiat_amount: new_order.fiat_amount, + premium: new_order.premium, + buyer_invoice: new_order.buyer_invoice.clone(), + created_at: Timestamp::now().as_u64() as i64, + expires_at: expiry_date, + ..Default::default() + }; + + match new_order.kind { + Some(OrderKind::Buy) => { + new_order_db.kind = OrderKind::Buy.to_string(); + new_order_db.buyer_pubkey = Some(trade_pubkey.to_string()); + new_order_db.master_buyer_pubkey = Some(identity_pubkey.to_string()); + new_order_db.trade_index_buyer = trade_index; + } + Some(OrderKind::Sell) => { + new_order_db.kind = OrderKind::Sell.to_string(); + new_order_db.seller_pubkey = Some(trade_pubkey.to_string()); + new_order_db.master_seller_pubkey = Some(identity_pubkey.to_string()); + new_order_db.trade_index_seller = trade_index; + } + None => { + send_cant_do_msg( + None, + None, + Some(CantDoReason::InvalidOrderKind), + &trade_pubkey, + ) + .await; + return None; + } + } + + // Request price from API in case amount is 0 + new_order_db.price_from_api = new_order.amount == 0; + Some(new_order_db) +} + pub async fn send_dm( receiver_pubkey: &PublicKey, sender_keys: Keys, - content: String, + payload: String, ) -> Result<()> { - let event = gift_wrap(&sender_keys, *receiver_pubkey, content.clone(), None)?; info!( - "Sending DM, Event ID: {} with content: {:#?}", - event.id, content + "sender key {} - receiver key {}", + sender_keys.public_key().to_hex(), + receiver_pubkey.to_hex() + ); + + let event = gift_wrap(&sender_keys, *receiver_pubkey, payload.clone(), None)?; + info!( + "Sending DM, Event ID: {} with payload: {:#?}", + event.id, payload ); if let Ok(client) = get_nostr_client() { - let _ = client.send_event(event).await; + if let Err(e) = client.send_event(event).await { + error!("Failed to send event: {}", e); + } } Ok(()) @@ -339,8 +395,10 @@ pub async fn connect_nostr() -> Result { let nostr_settings = Settings::get_nostr(); let mut limits = RelayLimits::default(); - limits.messages.max_size = Some(3_000); - limits.events.max_size = Some(3_500); + // Some specific events can have a bigger size than regular events + // So we increase the limits for those events + limits.messages.max_size = Some(6_000); + limits.events.max_size = Some(6_500); let opts = Options::new().relay_limits(limits); // Create new client @@ -403,12 +461,13 @@ pub async fn show_hold_invoice( request_id, Some(order.id), Action::PayInvoice, - Some(Content::PaymentRequest( + Some(Payload::PaymentRequest( Some(new_order), invoice_response.payment_request, None, )), seller_pubkey, + order.trade_index_seller, ) .await; // We send a message to buyer to know that seller was requested to pay the invoice @@ -418,6 +477,7 @@ pub async fn show_hold_invoice( Action::WaitingSellerToPay, None, buyer_pubkey, + order.trade_index_buyer, ) .await; @@ -519,8 +579,9 @@ pub async fn set_waiting_invoice_status( request_id, Some(order.id), Action::AddInvoice, - Some(Content::Order(order_data)), + Some(Payload::Order(order_data)), &buyer_pubkey, + order.trade_index_buyer, ) .await; @@ -536,7 +597,15 @@ pub async fn rate_counterpart( ) -> Result<()> { // Send dm to counterparts // to buyer - send_new_order_msg(request_id, Some(order.id), Action::Rate, None, buyer_pubkey).await; + send_new_order_msg( + request_id, + Some(order.id), + Action::Rate, + None, + buyer_pubkey, + None, + ) + .await; // to seller send_new_order_msg( request_id, @@ -544,6 +613,7 @@ pub async fn rate_counterpart( Action::Rate, None, seller_pubkey, + None, ) .await; @@ -561,8 +631,16 @@ pub async fn settle_seller_hold_invoice( request_id: Option, ) -> Result<()> { // Check if the pubkey is right - if !is_admin && event.sender.to_string() != *order.seller_pubkey.as_ref().unwrap().to_string() { - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + if !is_admin + && event.rumor.pubkey.to_string() != *order.seller_pubkey.as_ref().unwrap().to_string() + { + send_cant_do_msg( + request_id, + Some(order.id), + Some(CantDoReason::InvalidPeer), + &event.rumor.pubkey, + ) + .await; return Err(Error::msg("Not allowed")); } @@ -571,7 +649,13 @@ pub async fn settle_seller_hold_invoice( ln_client.settle_hold_invoice(preimage).await?; info!("{action}: Order Id {}: hold invoice settled", order.id); } else { - send_cant_do_msg(request_id, Some(order.id), None, &event.sender).await; + send_cant_do_msg( + request_id, + Some(order.id), + Some(CantDoReason::InvalidInvoice), + &event.rumor.pubkey, + ) + .await; return Err(Error::msg("No preimage")); } Ok(()) @@ -587,14 +671,11 @@ pub fn bytes_to_string(bytes: &[u8]) -> String { pub async fn send_cant_do_msg( request_id: Option, order_id: Option, - message: Option, + reason: Option, destination_key: &PublicKey, ) { - // Prepare content in case - let content = message.map(Content::TextMessage); - // Send message to event creator - let message = Message::cant_do(request_id, order_id, content); + let message = Message::cant_do(order_id, request_id, Some(Payload::CantDo(reason))); if let Ok(message) = message.as_json() { let sender_keys = crate::util::get_keys().unwrap(); let _ = send_dm(destination_key, sender_keys, message).await; @@ -605,11 +686,12 @@ pub async fn send_new_order_msg( request_id: Option, order_id: Option, action: Action, - content: Option, + payload: Option, destination_key: &PublicKey, + trade_index: Option, ) { // Send message to event creator - let message = Message::new_order(request_id, order_id, action, content); + let message = Message::new_order(order_id, request_id, trade_index, action, payload); if let Ok(message) = message.as_json() { let sender_keys = crate::util::get_keys().unwrap(); let _ = send_dm(destination_key, sender_keys, message).await; @@ -645,7 +727,7 @@ pub fn get_nostr_client() -> Result<&'static Client> { } /// Getter function with error management for nostr relays -pub async fn get_nostr_relays() -> Option> { +pub async fn get_nostr_relays() -> Option> { if let Some(client) = NOSTR_CLIENT.get() { Some(client.relays().await) } else { @@ -724,9 +806,17 @@ mod tests { initialize(); // Mock the send_dm function let receiver_pubkey = Keys::generate().public_key(); - let content = "Test message".to_string(); + let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23"); + let message = Message::Order(MessageKind::new( + Some(uuid), + None, + None, + Action::FiatSent, + None, + )); + let payload = message.as_json().unwrap(); let sender_keys = Keys::generate(); - let result = send_dm(&receiver_pubkey, sender_keys, content).await; + let result = send_dm(&receiver_pubkey, sender_keys, payload).await; assert!(result.is_ok()); } @@ -741,10 +831,11 @@ mod tests { ..Default::default() }; let message = Message::Order(MessageKind::new( - Some(1), Some(uuid), + Some(1), + Some(1), Action::TakeSell, - Some(Content::Amount(order.amount)), + Some(Payload::Amount(order.amount)), )); let amount = get_fiat_amount_requested(&order, &message); assert_eq!(amount, Some(1000));