From 705309b7748a83a72bc52aed0f54671f61ef2c42 Mon Sep 17 00:00:00 2001 From: "Javier G. Montoya S" Date: Fri, 28 Feb 2025 13:56:18 -0300 Subject: [PATCH 1/5] refactor(commands/invites): move commands to individual files --- .../{invites.rs => invites/accept_invite.rs} | 77 +------------------ .../src/commands/invites/decline_invite.rs | 35 +++++++++ src-tauri/src/commands/invites/get_invite.rs | 23 ++++++ src-tauri/src/commands/invites/get_invites.rs | 27 +++++++ src-tauri/src/commands/invites/mod.rs | 9 +++ 5 files changed, 95 insertions(+), 76 deletions(-) rename src-tauri/src/commands/{invites.rs => invites/accept_invite.rs} (59%) create mode 100644 src-tauri/src/commands/invites/decline_invite.rs create mode 100644 src-tauri/src/commands/invites/get_invite.rs create mode 100644 src-tauri/src/commands/invites/get_invites.rs create mode 100644 src-tauri/src/commands/invites/mod.rs diff --git a/src-tauri/src/commands/invites.rs b/src-tauri/src/commands/invites/accept_invite.rs similarity index 59% rename from src-tauri/src/commands/invites.rs rename to src-tauri/src/commands/invites/accept_invite.rs index 3462ffb..a1dc574 100644 --- a/src-tauri/src/commands/invites.rs +++ b/src-tauri/src/commands/invites/accept_invite.rs @@ -1,54 +1,10 @@ use crate::accounts::Account; use crate::groups::{Group, GroupType}; -use crate::invites::{Invite, InviteState, ProcessedInvite}; +use crate::invites::{Invite, InviteState}; use crate::whitenoise::Whitenoise; use nostr_sdk::prelude::*; -use serde::{Deserialize, Serialize}; use tauri::Emitter; -#[derive(Debug, Serialize, Deserialize)] -pub struct InvitesWithFailures { - invites: Vec, - failures: Vec<(EventId, String)>, -} - -/// Fetches invites from the database for the active user -#[tauri::command] -pub async fn get_invites(wn: tauri::State<'_, Whitenoise>) -> Result { - let pending_invites = Invite::pending(wn.clone()) - .await - .map_err(|e| e.to_string())?; - - let failed_invites: Vec<(EventId, String)> = ProcessedInvite::failed_with_reason(wn.clone()) - .await - .map_err(|e| e.to_string())?; - - Ok(InvitesWithFailures { - invites: pending_invites, - failures: failed_invites, - }) -} - -/// Gets a specific invite by its ID. -/// -/// # Arguments -/// * `invite_id` - The ID of the invite to retrieve -/// * `wn` - The Whitenoise state -/// -/// # Returns -/// * `Ok(Invite)` if the invite was found -/// * `Err(String)` if there was an error retrieving the invite or it wasn't found -#[tauri::command] -pub async fn get_invite( - active_account: String, - invite_id: String, - wn: tauri::State<'_, Whitenoise>, -) -> Result { - Invite::find_by_id(&active_account, &invite_id, wn.clone()) - .await - .map_err(|e| e.to_string()) -} - /// Accepts a group invite and joins the corresponding group. /// /// # Arguments @@ -143,34 +99,3 @@ pub async fn accept_invite( Ok(()) } - -/// Declines a group invite. -/// -/// # Arguments -/// * `invite` - The invite to decline -/// * `wn` - The Whitenoise state -/// * `app_handle` - The Tauri app handle -/// -/// # Returns -/// * `Ok(())` if the invite was successfully declined -/// * `Err(String)` if there was an error declining the invite -/// -/// # Events Emitted -/// * `invite_declined` - Emitted with the updated invite after it is declined -#[tauri::command] -pub async fn decline_invite( - mut invite: Invite, - wn: tauri::State<'_, Whitenoise>, - app_handle: tauri::AppHandle, -) -> Result<(), String> { - tracing::debug!(target: "whitenoise::invites::decline_invite", "Declining invite {:?}", invite.event.id.unwrap()); - - invite.state = InviteState::Declined; - invite.save(wn.clone()).await.map_err(|e| e.to_string())?; - - app_handle - .emit("invite_declined", invite) - .map_err(|e| e.to_string())?; - - Ok(()) -} diff --git a/src-tauri/src/commands/invites/decline_invite.rs b/src-tauri/src/commands/invites/decline_invite.rs new file mode 100644 index 0000000..c7d6dd9 --- /dev/null +++ b/src-tauri/src/commands/invites/decline_invite.rs @@ -0,0 +1,35 @@ +use crate::invites::{Invite, InviteState}; +use crate::whitenoise::Whitenoise; +use nostr_sdk::prelude::*; +use tauri::Emitter; + +/// Declines a group invite. +/// +/// # Arguments +/// * `invite` - The invite to decline +/// * `wn` - The Whitenoise state +/// * `app_handle` - The Tauri app handle +/// +/// # Returns +/// * `Ok(())` if the invite was successfully declined +/// * `Err(String)` if there was an error declining the invite +/// +/// # Events Emitted +/// * `invite_declined` - Emitted with the updated invite after it is declined +#[tauri::command] +pub async fn decline_invite( + mut invite: Invite, + wn: tauri::State<'_, Whitenoise>, + app_handle: tauri::AppHandle, +) -> Result<(), String> { + tracing::debug!(target: "whitenoise::invites::decline_invite", "Declining invite {:?}", invite.event.id.unwrap()); + + invite.state = InviteState::Declined; + invite.save(wn.clone()).await.map_err(|e| e.to_string())?; + + app_handle + .emit("invite_declined", invite) + .map_err(|e| e.to_string())?; + + Ok(()) +} diff --git a/src-tauri/src/commands/invites/get_invite.rs b/src-tauri/src/commands/invites/get_invite.rs new file mode 100644 index 0000000..d0a3fda --- /dev/null +++ b/src-tauri/src/commands/invites/get_invite.rs @@ -0,0 +1,23 @@ +use crate::invites::Invite; +use crate::whitenoise::Whitenoise; +use nostr_sdk::prelude::*; + +/// Gets a specific invite by its ID. +/// +/// # Arguments +/// * `invite_id` - The ID of the invite to retrieve +/// * `wn` - The Whitenoise state +/// +/// # Returns +/// * `Ok(Invite)` if the invite was found +/// * `Err(String)` if there was an error retrieving the invite or it wasn't found +#[tauri::command] +pub async fn get_invite( + active_account: String, + invite_id: String, + wn: tauri::State<'_, Whitenoise>, +) -> Result { + Invite::find_by_id(&active_account, &invite_id, wn.clone()) + .await + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/invites/get_invites.rs b/src-tauri/src/commands/invites/get_invites.rs new file mode 100644 index 0000000..b88bc76 --- /dev/null +++ b/src-tauri/src/commands/invites/get_invites.rs @@ -0,0 +1,27 @@ +use crate::invites::{Invite, ProcessedInvite}; +use crate::whitenoise::Whitenoise; +use nostr_sdk::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct InvitesWithFailures { + invites: Vec, + failures: Vec<(EventId, String)>, +} + +/// Fetches invites from the database for the active user +#[tauri::command] +pub async fn get_invites(wn: tauri::State<'_, Whitenoise>) -> Result { + let pending_invites = Invite::pending(wn.clone()) + .await + .map_err(|e| e.to_string())?; + + let failed_invites: Vec<(EventId, String)> = ProcessedInvite::failed_with_reason(wn.clone()) + .await + .map_err(|e| e.to_string())?; + + Ok(InvitesWithFailures { + invites: pending_invites, + failures: failed_invites, + }) +} diff --git a/src-tauri/src/commands/invites/mod.rs b/src-tauri/src/commands/invites/mod.rs new file mode 100644 index 0000000..3f5a47f --- /dev/null +++ b/src-tauri/src/commands/invites/mod.rs @@ -0,0 +1,9 @@ +mod accept_invite; +mod decline_invite; +mod get_invite; +mod get_invites; + +pub use accept_invite::accept_invite; +pub use decline_invite::decline_invite; +pub use get_invite::get_invite; +pub use get_invites::get_invites; From cb1e81caa21b1be4e041933e015b8996482bdc0e Mon Sep 17 00:00:00 2001 From: "Javier G. Montoya S" Date: Fri, 28 Feb 2025 14:06:29 -0300 Subject: [PATCH 2/5] refactor(commands/key_packages): move commands to individual files --- src-tauri/src/commands/key_packages.rs | 111 ------------------ .../key_packages/delete_all_key_packages.rs | 52 ++++++++ src-tauri/src/commands/key_packages/mod.rs | 7 ++ .../key_packages/publish_new_key_package.rs | 34 ++++++ .../valid_key_package_exists_for_user.rs | 28 +++++ 5 files changed, 121 insertions(+), 111 deletions(-) delete mode 100644 src-tauri/src/commands/key_packages.rs create mode 100644 src-tauri/src/commands/key_packages/delete_all_key_packages.rs create mode 100644 src-tauri/src/commands/key_packages/mod.rs create mode 100644 src-tauri/src/commands/key_packages/publish_new_key_package.rs create mode 100644 src-tauri/src/commands/key_packages/valid_key_package_exists_for_user.rs diff --git a/src-tauri/src/commands/key_packages.rs b/src-tauri/src/commands/key_packages.rs deleted file mode 100644 index f788ca4..0000000 --- a/src-tauri/src/commands/key_packages.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::accounts::Account; -use crate::key_packages::{fetch_key_package_for_pubkey, publish_key_package}; -use crate::relays::RelayType; -use crate::Whitenoise; -use nostr_sdk::event::EventBuilder; - -/// Checks if a valid MLS key package exists for a given user -/// -/// # Arguments -/// * `pubkey` - Hex encoded Nostr public key of the user to check -/// * `wn` - Whitenoise state containing Nostr client -/// -/// # Returns -/// * `Ok(bool)` - True if valid key package exists, false otherwise -/// * `Err(String)` - Error message if check fails -/// -/// # Errors -/// Returns error if: -/// - Public key is invalid -/// - Network error occurs fetching key package -/// - Key package parsing fails -#[tauri::command] -pub async fn valid_key_package_exists_for_user( - pubkey: String, - wn: tauri::State<'_, Whitenoise>, -) -> Result { - let key_package = fetch_key_package_for_pubkey(pubkey, wn.clone()) - .await - .map_err(|e| e.to_string())?; - Ok(key_package.is_some()) -} - -/// Publishes a new MLS key package for the active account to Nostr -/// -/// Creates and publishes a new MLS key package event to the user's configured key package relays. -/// The key package contains the necessary cryptographic material for adding the user to MLS groups. -/// -/// # Arguments -/// * `wn` - Whitenoise state containing account and Nostr clients -/// -/// # Returns -/// * `Ok(())` - Key package was successfully published -/// * `Err(String)` - Error message if publishing fails -/// -/// # Flow -/// 1. Gets active account's public key -/// 2. Creates new MLS key package -/// 3. Gets configured key package relays -/// 4. Builds Nostr event with key package and metadata -/// 5. Publishes event to relays -/// -/// # Errors -/// Returns error if: -/// - No active account found -/// - Key package relays not configured -/// - Key package creation fails -/// - Event publishing fails -#[tauri::command] -pub async fn publish_new_key_package(wn: tauri::State<'_, Whitenoise>) -> Result<(), String> { - publish_key_package(wn.clone()) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn delete_all_key_packages(wn: tauri::State<'_, Whitenoise>) -> Result<(), String> { - let pubkey = wn - .nostr - .client - .signer() - .await - .map_err(|e| e.to_string())? - .get_public_key() - .await - .map_err(|e| e.to_string())?; - - let active_account = Account::get_active(wn.clone()) - .await - .map_err(|e| e.to_string())?; - - let key_package_relays: Vec = if cfg!(dev) { - vec!["ws://localhost:8080".to_string()] - } else { - active_account - .relays(RelayType::KeyPackage, wn.clone()) - .await - .map_err(|e| e.to_string())? - }; - - let key_package_events = wn - .nostr - .query_user_key_packages(pubkey) - .await - .map_err(|e| e.to_string())?; - - if !key_package_events.is_empty() { - let delete_event = EventBuilder::delete_with_reason( - key_package_events.iter().map(|e| e.id), - "Delete own key package", - ); - tracing::debug!(target: "whitenoise::commands::key_packages::delete_all_key_packages", "Deleting key packages: {:?}", delete_event); - wn.nostr - .client - .send_event_builder_to(key_package_relays, delete_event) - .await - .map_err(|e| e.to_string())?; - } else { - tracing::debug!(target: "whitenoise::commands::key_packages::delete_all_key_packages", "No key packages to delete"); - } - Ok(()) -} diff --git a/src-tauri/src/commands/key_packages/delete_all_key_packages.rs b/src-tauri/src/commands/key_packages/delete_all_key_packages.rs new file mode 100644 index 0000000..017d63d --- /dev/null +++ b/src-tauri/src/commands/key_packages/delete_all_key_packages.rs @@ -0,0 +1,52 @@ +use crate::accounts::Account; +use crate::relays::RelayType; +use crate::Whitenoise; +use nostr_sdk::event::EventBuilder; + +#[tauri::command] +pub async fn delete_all_key_packages(wn: tauri::State<'_, Whitenoise>) -> Result<(), String> { + let pubkey = wn + .nostr + .client + .signer() + .await + .map_err(|e| e.to_string())? + .get_public_key() + .await + .map_err(|e| e.to_string())?; + + let active_account = Account::get_active(wn.clone()) + .await + .map_err(|e| e.to_string())?; + + let key_package_relays: Vec = if cfg!(dev) { + vec!["ws://localhost:8080".to_string()] + } else { + active_account + .relays(RelayType::KeyPackage, wn.clone()) + .await + .map_err(|e| e.to_string())? + }; + + let key_package_events = wn + .nostr + .query_user_key_packages(pubkey) + .await + .map_err(|e| e.to_string())?; + + if !key_package_events.is_empty() { + let delete_event = EventBuilder::delete_with_reason( + key_package_events.iter().map(|e| e.id), + "Delete own key package", + ); + tracing::debug!(target: "whitenoise::commands::key_packages::delete_all_key_packages", "Deleting key packages: {:?}", delete_event); + wn.nostr + .client + .send_event_builder_to(key_package_relays, delete_event) + .await + .map_err(|e| e.to_string())?; + } else { + tracing::debug!(target: "whitenoise::commands::key_packages::delete_all_key_packages", "No key packages to delete"); + } + Ok(()) +} diff --git a/src-tauri/src/commands/key_packages/mod.rs b/src-tauri/src/commands/key_packages/mod.rs new file mode 100644 index 0000000..b18aa48 --- /dev/null +++ b/src-tauri/src/commands/key_packages/mod.rs @@ -0,0 +1,7 @@ +mod delete_all_key_packages; +mod publish_new_key_package; +mod valid_key_package_exists_for_user; + +pub use delete_all_key_packages::delete_all_key_packages; +pub use publish_new_key_package::publish_new_key_package; +pub use valid_key_package_exists_for_user::valid_key_package_exists_for_user; diff --git a/src-tauri/src/commands/key_packages/publish_new_key_package.rs b/src-tauri/src/commands/key_packages/publish_new_key_package.rs new file mode 100644 index 0000000..293fc1c --- /dev/null +++ b/src-tauri/src/commands/key_packages/publish_new_key_package.rs @@ -0,0 +1,34 @@ +use crate::key_packages::publish_key_package; +use crate::Whitenoise; + +/// Publishes a new MLS key package for the active account to Nostr +/// +/// Creates and publishes a new MLS key package event to the user's configured key package relays. +/// The key package contains the necessary cryptographic material for adding the user to MLS groups. +/// +/// # Arguments +/// * `wn` - Whitenoise state containing account and Nostr clients +/// +/// # Returns +/// * `Ok(())` - Key package was successfully published +/// * `Err(String)` - Error message if publishing fails +/// +/// # Flow +/// 1. Gets active account's public key +/// 2. Creates new MLS key package +/// 3. Gets configured key package relays +/// 4. Builds Nostr event with key package and metadata +/// 5. Publishes event to relays +/// +/// # Errors +/// Returns error if: +/// - No active account found +/// - Key package relays not configured +/// - Key package creation fails +/// - Event publishing fails +#[tauri::command] +pub async fn publish_new_key_package(wn: tauri::State<'_, Whitenoise>) -> Result<(), String> { + publish_key_package(wn.clone()) + .await + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/key_packages/valid_key_package_exists_for_user.rs b/src-tauri/src/commands/key_packages/valid_key_package_exists_for_user.rs new file mode 100644 index 0000000..f9e0834 --- /dev/null +++ b/src-tauri/src/commands/key_packages/valid_key_package_exists_for_user.rs @@ -0,0 +1,28 @@ +use crate::key_packages::fetch_key_package_for_pubkey; +use crate::Whitenoise; + +/// Checks if a valid MLS key package exists for a given user +/// +/// # Arguments +/// * `pubkey` - Hex encoded Nostr public key of the user to check +/// * `wn` - Whitenoise state containing Nostr client +/// +/// # Returns +/// * `Ok(bool)` - True if valid key package exists, false otherwise +/// * `Err(String)` - Error message if check fails +/// +/// # Errors +/// Returns error if: +/// - Public key is invalid +/// - Network error occurs fetching key package +/// - Key package parsing fails +#[tauri::command] +pub async fn valid_key_package_exists_for_user( + pubkey: String, + wn: tauri::State<'_, Whitenoise>, +) -> Result { + let key_package = fetch_key_package_for_pubkey(pubkey, wn.clone()) + .await + .map_err(|e| e.to_string())?; + Ok(key_package.is_some()) +} From b620b3eee3e35bfa4fd2a154bfe22a55d0dcb66c Mon Sep 17 00:00:00 2001 From: "Javier G. Montoya S" Date: Fri, 28 Feb 2025 14:08:00 -0300 Subject: [PATCH 3/5] refactor(commands/messages): move command to its own file within messages dir --- src-tauri/src/commands/messages/mod.rs | 3 +++ .../src/commands/{messages.rs => messages/query_message.rs} | 0 2 files changed, 3 insertions(+) create mode 100644 src-tauri/src/commands/messages/mod.rs rename src-tauri/src/commands/{messages.rs => messages/query_message.rs} (100%) diff --git a/src-tauri/src/commands/messages/mod.rs b/src-tauri/src/commands/messages/mod.rs new file mode 100644 index 0000000..c727e4e --- /dev/null +++ b/src-tauri/src/commands/messages/mod.rs @@ -0,0 +1,3 @@ +mod query_message; + +pub use query_message::query_message; diff --git a/src-tauri/src/commands/messages.rs b/src-tauri/src/commands/messages/query_message.rs similarity index 100% rename from src-tauri/src/commands/messages.rs rename to src-tauri/src/commands/messages/query_message.rs From 66e3552b35fc6fe6dbe965e0338d81ed755647f9 Mon Sep 17 00:00:00 2001 From: "Javier G. Montoya S" Date: Fri, 28 Feb 2025 14:15:02 -0300 Subject: [PATCH 4/5] refactor(commands/payments): move pay_invoice command to its own file within payments dir --- src-tauri/src/commands/payments/mod.rs | 3 +++ .../src/commands/{payments.rs => payments/pay_invoice.rs} | 0 2 files changed, 3 insertions(+) create mode 100644 src-tauri/src/commands/payments/mod.rs rename src-tauri/src/commands/{payments.rs => payments/pay_invoice.rs} (100%) diff --git a/src-tauri/src/commands/payments/mod.rs b/src-tauri/src/commands/payments/mod.rs new file mode 100644 index 0000000..b46e0ef --- /dev/null +++ b/src-tauri/src/commands/payments/mod.rs @@ -0,0 +1,3 @@ +mod pay_invoice; + +pub use pay_invoice::pay_invoice; diff --git a/src-tauri/src/commands/payments.rs b/src-tauri/src/commands/payments/pay_invoice.rs similarity index 100% rename from src-tauri/src/commands/payments.rs rename to src-tauri/src/commands/payments/pay_invoice.rs From 25add68c9db31539c5d351d12952ec2804de9846 Mon Sep 17 00:00:00 2001 From: "Javier G. Montoya S" Date: Fri, 28 Feb 2025 14:40:03 -0300 Subject: [PATCH 5/5] test(commands/payments/pay_invoice): refactor command for improved testability --- src-tauri/Cargo.lock | 5 +- src-tauri/Cargo.toml | 1 + .../src/commands/payments/pay_invoice.rs | 224 +++++++++++++++++- 3 files changed, 220 insertions(+), 10 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 72a0b4b..1b92f41 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -261,9 +261,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", @@ -6843,6 +6843,7 @@ dependencies = [ name = "whitenoise" version = "0.1.0" dependencies = [ + "async-trait", "base64 0.22.1", "chrono", "keyring", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b3fd51b..b5462ff 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,6 +43,7 @@ tauri-plugin-clipboard-manager = "2.2.1" tauri-plugin-notification = "2.2.1" nwc = { version = "0.38" } lightning-invoice = "0.33.1" +async-trait = "0.1.86" [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] nostr-sdk = { version = "0.38", features = [ diff --git a/src-tauri/src/commands/payments/pay_invoice.rs b/src-tauri/src/commands/payments/pay_invoice.rs index c5123e1..6a0b84f 100644 --- a/src-tauri/src/commands/payments/pay_invoice.rs +++ b/src-tauri/src/commands/payments/pay_invoice.rs @@ -41,19 +41,227 @@ pub async fn pay_invoice( .map_err(|_| CommandError::NoNWCUri)? .ok_or(CommandError::NoNWCUri)?; - let preimage = payments::pay_bolt11_invoice(&bolt11, &nwc_uri) + let payment_service = DefaultPaymentService; + let message_params = pay_invoice_and_get_msg_params(&payment_service, tags, &bolt11, &nwc_uri) + .await + .map_err(|_| CommandError::MessageError)?; + + let unsigned_message = send_mls_message( + group, + message_params.message, + message_params.kind, + message_params.tags, + wn, + app_handle, + ) + .await + .map_err(|_| CommandError::MessageError)?; + + Ok(unsigned_message) +} + +struct MlsMessageParams { + message: String, + kind: u16, + tags: Option>, +} + +#[async_trait::async_trait] +pub trait PaymentService: Send + Sync { + /// Pay a BOLT11 invoice and return the preimage + async fn pay_bolt11_invoice(&self, bolt11: &str, nwc_uri: &str) + -> Result; +} + +struct DefaultPaymentService; + +#[async_trait::async_trait] +impl PaymentService for DefaultPaymentService { + async fn pay_bolt11_invoice( + &self, + bolt11: &str, + nwc_uri: &str, + ) -> Result { + payments::pay_bolt11_invoice(bolt11, nwc_uri).await + } +} + +async fn pay_invoice_and_get_msg_params( + payment_service: &impl PaymentService, + tags: Option>, + bolt11: &str, + nwc_uri: &str, +) -> Result { + let preimage = payment_service + .pay_bolt11_invoice(bolt11, nwc_uri) .await .map_err(CommandError::from)?; - let message = "".to_string(); - let kind = 9; + Ok(MlsMessageParams { + message: "".to_string(), + kind: 9, + tags: Some(create_payment_tags(tags, &preimage.to_string())), + }) +} + +fn create_payment_tags(tags: Option>, preimage: &str) -> Vec { let mut final_tags = tags.unwrap_or_default(); final_tags.push(Tag::custom( TagKind::Custom("preimage".into()), vec![preimage.to_string()], )); - let final_tags = Some(final_tags); - let unsigned_message = send_mls_message(group, message, kind, final_tags, wn, app_handle) - .await - .map_err(|_| CommandError::MessageError)?; - Ok(unsigned_message) + final_tags +} + +#[cfg(test)] +mod tests { + use super::*; + + struct MockPaymentService { + expected_bolt11: String, + expected_nwc_uri: String, + result: Option, + error_message: Option, + } + + impl MockPaymentService { + fn new(bolt11: &str, nwc_uri: &str, result: Result) -> Self { + match result { + Ok(preimage) => Self { + expected_bolt11: bolt11.to_string(), + expected_nwc_uri: nwc_uri.to_string(), + result: Some(preimage), + error_message: None, + }, + Err(error) => Self { + expected_bolt11: bolt11.to_string(), + expected_nwc_uri: nwc_uri.to_string(), + result: None, + error_message: Some(error), + }, + } + } + } + + #[async_trait::async_trait] + impl PaymentService for MockPaymentService { + async fn pay_bolt11_invoice( + &self, + bolt11: &str, + nwc_uri: &str, + ) -> Result { + assert_eq!( + bolt11, self.expected_bolt11, + "bolt11 parameter doesn't match expected value" + ); + assert_eq!( + nwc_uri, self.expected_nwc_uri, + "nwc_uri parameter doesn't match expected value" + ); + + match &self.result { + Some(preimage) => Ok(preimage.clone()), + None => { + let error_msg = self + .error_message + .as_ref() + .unwrap_or(&"Unknown error".to_string()) + .clone(); + Err(PaymentError::PaymentFailure(error_msg)) + } + } + } + } + + #[tokio::test] + async fn test_pay_invoice_and_get_msg_params_success() { + let tags = Some(vec![Tag::custom( + TagKind::Custom("test".into()), + vec!["value".to_string()], + )]); + let bolt11 = "lnbc1invoice"; + let nwc_uri = "nostr+walletconnect://test"; + let expected_preimage = "0123456789abcdef".to_string(); + + let mock_service = MockPaymentService::new(bolt11, nwc_uri, Ok(expected_preimage.clone())); + let result = + pay_invoice_and_get_msg_params(&mock_service, tags.clone(), bolt11, nwc_uri).await; + + assert!(result.is_ok(), "Expected successful result"); + let params = result.unwrap(); + assert_eq!(params.message, "", "Message should be empty"); + assert_eq!(params.kind, 9, "Kind should be 9"); + + let final_tags = params.tags.unwrap(); + + let expected_tags = vec![ + tags.clone().unwrap()[0].clone(), + Tag::custom( + TagKind::Custom("preimage".into()), + vec![expected_preimage.clone()], + ), + ]; + + assert_eq!( + final_tags.len(), + expected_tags.len(), + "Should have 2 tags (original + preimage)" + ); + + for i in 0..expected_tags.len() { + assert_eq!( + final_tags[i], expected_tags[i], + "Tag at position {} doesn't match expected tag", + i + ); + } + } + + #[tokio::test] + async fn test_pay_invoice_and_get_msg_params_no_tags() { + let bolt11 = "lnbc1invoice"; + let nwc_uri = "nostr+walletconnect://test"; + let expected_preimage = "0123456789abcdef".to_string(); + + let mock_service = MockPaymentService::new(bolt11, nwc_uri, Ok(expected_preimage.clone())); + let result = pay_invoice_and_get_msg_params(&mock_service, None, bolt11, nwc_uri).await; + + assert!(result.is_ok(), "Expected successful result"); + let params = result.unwrap(); + assert_eq!(params.message, "", "Message should be empty"); + assert_eq!(params.kind, 9, "Kind should be 9"); + + let final_tags = params.tags.unwrap(); + assert_eq!(final_tags.len(), 1, "Should have only the preimage tag"); + + let expected_tag = Tag::custom( + TagKind::Custom("preimage".into()), + vec![expected_preimage.clone()], + ); + assert_eq!( + final_tags[0], expected_tag, + "Tag should be the preimage tag" + ); + } + + #[tokio::test] + async fn test_pay_invoice_and_get_msg_params_payment_error() { + let bolt11 = "lnbc1invoice"; + let nwc_uri = "nostr+walletconnect://test"; + let error_message = "Payment failed".to_string(); + + let mock_service = MockPaymentService::new(bolt11, nwc_uri, Err(error_message.clone())); + let result = pay_invoice_and_get_msg_params(&mock_service, None, bolt11, nwc_uri).await; + + assert!(result.is_err(), "Expected error result"); + match result { + Err(CommandError::PaymentError(err)) => { + assert!( + err.contains(&error_message), + "Error message should contain '{}'", + error_message + ); + } + _ => panic!("Expected PaymentError"), + } + } }