From 7cd6c55a21f9243978fbc71c2f0d81f91153393c Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 10 Apr 2024 20:22:56 -0500 Subject: [PATCH 1/7] Mock primal client --- mutiny-core/src/lib.rs | 12 +++++- mutiny-core/src/nostr/mod.rs | 73 ++++++++++++++++----------------- mutiny-core/src/nostr/nwc.rs | 40 +++++++++--------- mutiny-core/src/nostr/primal.rs | 37 ++++++++++++++--- 4 files changed, 98 insertions(+), 64 deletions(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 3f138e257..500c68b15 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -46,6 +46,7 @@ mod test_utils; pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY}; pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{CHANNEL_CLOSURE_PREFIX, CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY}; +use crate::nostr::primal::{PrimalApi, PrimalClient}; use crate::storage::{ get_payment_hash_from_key, list_payment_info, persist_payment_info, update_nostr_contact_list, IndexItem, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY, @@ -848,12 +849,19 @@ impl MutinyWalletBuilder { // start syncing node manager NodeManager::start_sync(node_manager.clone()); + let primal_client = PrimalClient::new( + config + .primal_url + .clone() + .unwrap_or("https://primal-cache.mutinywallet.com/api".to_string()), + ); + // create nostr manager let nostr = Arc::new(NostrManager::from_mnemonic( self.xprivkey, self.nostr_key_source, self.storage.clone(), - config.primal_url.clone(), + primal_client, logger.clone(), stop.clone(), )?); @@ -1099,7 +1107,7 @@ pub struct MutinyWallet { config: MutinyWalletConfig, pub(crate) storage: S, pub node_manager: Arc>, - pub nostr: Arc>, + pub nostr: Arc>, pub federation_storage: Arc>, pub(crate) federations: Arc>>>>, lnurl_client: Arc, diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 12878900c..f1a3eb740 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -6,7 +6,7 @@ use crate::nostr::nwc::{ NwcProfile, NwcProfileTag, PendingNwcInvoice, Profile, SingleUseSpendingConditions, SpendingConditions, PENDING_NWC_EVENTS_KEY, }; -use crate::nostr::primal::PrimalClient; +use crate::nostr::primal::PrimalApi; use crate::storage::{update_nostr_contact_list, MutinyStorage, NOSTR_CONTACT_LIST}; use crate::{error::MutinyError, utils::get_random_bip32_child_index}; use crate::{labels::LabelStorage, InvoiceHandler}; @@ -41,7 +41,7 @@ use url::Url; pub mod nip49; pub mod nwc; -mod primal; +pub(crate) mod primal; const PROFILE_ACCOUNT_INDEX: u32 = 0; const NWC_ACCOUNT_INDEX: u32 = 1; @@ -149,7 +149,7 @@ impl NostrKeys { /// Manages Nostr keys and has different utilities for nostr specific things #[derive(Clone)] -pub struct NostrManager { +pub struct NostrManager { /// Extended private key that is the root seed of the wallet xprivkey: ExtendedPrivKey, /// Primary key used for nostr, this will be used for signing events @@ -168,7 +168,7 @@ pub struct NostrManager { /// Nostr client pub client: Client, /// Primal client - pub primal_client: PrimalClient, + pub primal_client: P, } /// A fedimint we discovered on nostr @@ -212,7 +212,7 @@ impl Ord for NostrDiscoveredFedimint { } } -impl NostrManager { +impl NostrManager { /// Connect to the nostr relays pub async fn connect(&self) -> Result<(), MutinyError> { self.client.add_relays(self.get_relays()).await?; @@ -2081,37 +2081,12 @@ impl NostrManager { Ok(mints) } - /// Derives the client and server keys for Nostr Wallet Connect given a profile index - /// The left key is the client key and the right key is the server key - pub(crate) fn derive_nwc_keys( - context: &Secp256k1, - xprivkey: ExtendedPrivKey, - profile_index: u32, - ) -> Result<(Keys, Keys), MutinyError> { - let client_key = derive_nostr_key( - context, - xprivkey, - NWC_ACCOUNT_INDEX, - Some(profile_index), - Some(0), - )?; - let server_key = derive_nostr_key( - context, - xprivkey, - NWC_ACCOUNT_INDEX, - Some(profile_index), - Some(1), - )?; - - Ok((client_key, server_key)) - } - /// Creates a new NostrManager pub fn from_mnemonic( xprivkey: ExtendedPrivKey, key_source: NostrKeySource, storage: S, - primal_url: Option, + primal_api: P, logger: Arc, stop: Arc, ) -> Result { @@ -2130,10 +2105,6 @@ impl NostrManager { let client = Client::new(nostr_keys.signer.clone()); - let primal_client = PrimalClient::new( - primal_url.unwrap_or("https://primal-cache.mutinywallet.com/api".to_string()), - ); - Ok(Self { xprivkey, nostr_keys: Arc::new(async_lock::RwLock::new(nostr_keys)), @@ -2141,7 +2112,7 @@ impl NostrManager { storage, pending_nwc_lock: Arc::new(Mutex::new(())), follow_lock: Arc::new(Mutex::new(())), - primal_client, + primal_client: primal_api, logger, stop, client, @@ -2149,6 +2120,31 @@ impl NostrManager { } } +/// Derives the client and server keys for Nostr Wallet Connect given a profile index +/// The left key is the client key and the right key is the server key +pub(crate) fn derive_nwc_keys( + context: &Secp256k1, + xprivkey: ExtendedPrivKey, + profile_index: u32, +) -> Result<(Keys, Keys), MutinyError> { + let client_key = derive_nostr_key( + context, + xprivkey, + NWC_ACCOUNT_INDEX, + Some(profile_index), + Some(0), + )?; + let server_key = derive_nostr_key( + context, + xprivkey, + NWC_ACCOUNT_INDEX, + Some(profile_index), + Some(1), + )?; + + Ok((client_key, server_key)) +} + pub fn derive_nostr_key( context: &Secp256k1, xprivkey: ExtendedPrivKey, @@ -2240,6 +2236,7 @@ fn is_federation_recommendation_event(event: &Event) -> bool { #[cfg(test)] mod test { use super::*; + use crate::nostr::primal::MockPrimalApi; use crate::storage::MemoryStorage; use crate::utils::now; use crate::MockInvoiceHandler; @@ -2256,7 +2253,7 @@ mod test { const EXPIRED_INVOICE: &str = "lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm"; - fn create_nostr_manager() -> NostrManager { + fn create_nostr_manager() -> NostrManager { let mnemonic = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").expect("could not generate"); let xprivkey = @@ -2272,7 +2269,7 @@ mod test { xprivkey, NostrKeySource::Derived, storage, - None, + MockPrimalApi::new(), logger, stop, ) diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index 559af4fff..c12276708 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -1,7 +1,8 @@ use crate::error::MutinyError; use crate::event::HTLCStatus; use crate::nostr::nip49::NIP49Confirmation; -use crate::nostr::NostrManager; +use crate::nostr::primal::PrimalApi; +use crate::nostr::{derive_nwc_keys, NostrManager}; use crate::storage::MutinyStorage; use crate::utils; use crate::InvoiceHandler; @@ -238,7 +239,7 @@ impl NostrWalletConnect { let key_derivation_index = profile.child_key_index.unwrap_or(profile.index); let (derived_client_key, server_key) = - NostrManager::<()>::derive_nwc_keys(context, xprivkey, key_derivation_index)?; + derive_nwc_keys(context, xprivkey, key_derivation_index)?; // if the profile has a client key, we should use that instead of the derived one, that means // that the profile was created from NWA @@ -359,9 +360,9 @@ impl NostrWalletConnect { } } - async fn save_pending_nwc_invoice( + async fn save_pending_nwc_invoice( &self, - nostr_manager: &NostrManager, + nostr_manager: &NostrManager, event_id: EventId, event_pk: nostr::PublicKey, invoice: Bolt11Invoice, @@ -409,11 +410,11 @@ impl NostrWalletConnect { /// Handle a Nostr Wallet Connect request /// /// Returns a response event if one is needed - pub async fn handle_nwc_request( + pub async fn handle_nwc_request( &mut self, event: Event, node: &impl InvoiceHandler, - nostr_manager: &NostrManager, + nostr_manager: &NostrManager, ) -> anyhow::Result> { let client_pubkey = self.client_key.public_key(); let mut needs_save = false; @@ -721,11 +722,11 @@ impl NostrWalletConnect { Ok(Some(response)) } - async fn handle_pay_invoice_request( + async fn handle_pay_invoice_request( &mut self, event: Event, node: &impl InvoiceHandler, - nostr_manager: &NostrManager, + nostr_manager: &NostrManager, params: PayInvoiceRequestParams, needs_delete: &mut bool, needs_save: &mut bool, @@ -1486,6 +1487,7 @@ mod test { mod wasm_test { use super::*; use crate::logging::MutinyLogger; + use crate::nostr::primal::MockPrimalApi; use crate::nostr::{NostrKeySource, ProfileType}; use crate::storage::MemoryStorage; use crate::test_utils::{ @@ -1538,7 +1540,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), mw.logger.clone(), stop, ) @@ -1597,7 +1599,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), logger.clone(), stop, ) @@ -1804,7 +1806,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), Arc::new(MutinyLogger::default()), stop, ) @@ -1882,7 +1884,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), mw.logger.clone(), stop, ) @@ -1971,7 +1973,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), logger, stop, ) @@ -2042,7 +2044,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), mw.logger.clone(), stop, ) @@ -2087,7 +2089,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), mw.logger.clone(), stop, ) @@ -2139,7 +2141,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), mw.logger.clone(), stop, ) @@ -2188,7 +2190,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), Arc::new(MutinyLogger::default()), stop, ) @@ -2251,7 +2253,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), Arc::new(MutinyLogger::default()), stop, ) @@ -2316,7 +2318,7 @@ mod wasm_test { xprivkey, NostrKeySource::Derived, storage.clone(), - None, + MockPrimalApi::new(), Arc::new(MutinyLogger::default()), stop, ) diff --git a/mutiny-core/src/nostr/primal.rs b/mutiny-core/src/nostr/primal.rs index 68ba724de..e15c02d9e 100644 --- a/mutiny-core/src/nostr/primal.rs +++ b/mutiny-core/src/nostr/primal.rs @@ -5,6 +5,31 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; +#[cfg_attr(test, mockall::automock)] +pub trait PrimalApi { + async fn get_user_profile( + &self, + npub: nostr::PublicKey, + ) -> Result, MutinyError>; + async fn get_user_profiles( + &self, + npubs: Vec, + ) -> Result, MutinyError>; + async fn get_nostr_contacts( + &self, + npub: nostr::PublicKey, + ) -> Result<(Option, HashMap), MutinyError>; + async fn get_dm_conversation( + &self, + npub1: nostr::PublicKey, + npub2: nostr::PublicKey, + limit: u64, + until: Option, + since: Option, + ) -> Result, MutinyError>; + async fn get_trusted_users(&self, limit: u32) -> Result, MutinyError>; +} + #[derive(Debug, Clone)] pub struct PrimalClient { api_url: String, @@ -32,8 +57,10 @@ impl PrimalClient { .await .map_err(|_| MutinyError::NostrError) } +} - pub async fn get_user_profile( +impl PrimalApi for PrimalClient { + async fn get_user_profile( &self, npub: nostr::PublicKey, ) -> Result, MutinyError> { @@ -57,7 +84,7 @@ impl PrimalClient { Ok(None) } - pub async fn get_user_profiles( + async fn get_user_profiles( &self, npubs: Vec, ) -> Result, MutinyError> { @@ -66,7 +93,7 @@ impl PrimalClient { Ok(parse_profile_metadata(data)) } - pub async fn get_nostr_contacts( + async fn get_nostr_contacts( &self, npub: nostr::PublicKey, ) -> Result<(Option, HashMap), MutinyError> { @@ -84,7 +111,7 @@ impl PrimalClient { Ok((contact_list, parse_profile_metadata(data))) } - pub async fn get_dm_conversation( + async fn get_dm_conversation( &self, npub1: nostr::PublicKey, npub2: nostr::PublicKey, @@ -125,7 +152,7 @@ impl PrimalClient { } /// Returns a list of trusted users from primal with their trust rating - pub async fn get_trusted_users(&self, limit: u32) -> Result, MutinyError> { + async fn get_trusted_users(&self, limit: u32) -> Result, MutinyError> { let body = json!(["trusted_users", {"limit": limit }]); let data: Vec = self.primal_request(body).await?; From 181da99c870daf7fe7dfddf1260ac7852c334e40 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Thu, 11 Apr 2024 15:45:02 -0500 Subject: [PATCH 2/7] Mock Nostr Client --- mutiny-core/src/lib.rs | 28 ++++---- mutiny-core/src/nostr/client.rs | 109 ++++++++++++++++++++++++++++++++ mutiny-core/src/nostr/mod.rs | 62 ++++++++++++------ mutiny-core/src/nostr/nwc.rs | 42 ++++++++++-- 4 files changed, 203 insertions(+), 38 deletions(-) create mode 100644 mutiny-core/src/nostr/client.rs diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 500c68b15..5bb3fa825 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -110,7 +110,7 @@ use moksha_core::primitives::{ PostMeltQuoteBolt11Response, }; use moksha_core::token::TokenV3; -use nostr_sdk::{NostrSigner, RelayPoolNotification}; +use nostr_sdk::{Client, NostrSigner, RelayPoolNotification}; use reqwest::multipart::{Form, Part}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -856,15 +856,21 @@ impl MutinyWalletBuilder { .unwrap_or("https://primal-cache.mutinywallet.com/api".to_string()), ); + let client = Client::default(); + // create nostr manager - let nostr = Arc::new(NostrManager::from_mnemonic( - self.xprivkey, - self.nostr_key_source, - self.storage.clone(), - primal_client, - logger.clone(), - stop.clone(), - )?); + let nostr = Arc::new( + NostrManager::from_mnemonic( + self.xprivkey, + self.nostr_key_source, + self.storage.clone(), + primal_client, + client, + logger.clone(), + stop.clone(), + ) + .await?, + ); // connect to relays when not in tests #[cfg(not(test))] @@ -1107,7 +1113,7 @@ pub struct MutinyWallet { config: MutinyWalletConfig, pub(crate) storage: S, pub node_manager: Arc>, - pub nostr: Arc>, + pub nostr: Arc>, pub federation_storage: Arc>, pub(crate) federations: Arc>>>>, lnurl_client: Arc, @@ -1180,7 +1186,7 @@ impl MutinyWallet { log_warn!(logger, "Failed to clear invalid NWC invoices: {e}"); } - let client = &nostr.client; + let client = nostr_sdk::Client::default(); client .add_relays(nostr.get_relays()) diff --git a/mutiny-core/src/nostr/client.rs b/mutiny-core/src/nostr/client.rs new file mode 100644 index 000000000..b05090744 --- /dev/null +++ b/mutiny-core/src/nostr/client.rs @@ -0,0 +1,109 @@ +use nostr::{Event, EventBuilder, EventId, Filter, PublicKey, SubscriptionId}; +use nostr_sdk::client::Error; +use nostr_sdk::{NostrSigner, SubscribeAutoCloseOptions}; +use std::time::Duration; + +#[cfg_attr(test, mockall::automock)] +pub trait NostrClient { + async fn add_relays(&self, relays: Vec) -> nostr::Result<(), Error>; + async fn add_relay(&self, relay: &str) -> nostr::Result; + async fn connect_relay(&self, relay: &str) -> nostr::Result<(), Error>; + async fn connect(&self); + async fn disconnect(&self) -> nostr::Result<(), Error>; + + async fn sign_event_builder(&self, builder: EventBuilder) -> nostr::Result; + async fn send_event_builder(&self, builder: EventBuilder) -> nostr::Result; + async fn send_event(&self, event: Event) -> nostr::Result; + async fn send_event_to(&self, urls: Vec, event: Event) + -> nostr::Result; + async fn send_direct_msg( + &self, + receiver: PublicKey, + msg: String, + reply_to: Option, + ) -> nostr::Result; + + async fn subscribe( + &self, + filters: Vec, + opts: Option, + ) -> SubscriptionId; + async fn get_events_of( + &self, + filters: Vec, + timeout: Option, + ) -> Result, Error>; + + async fn set_signer(&self, signer: Option); +} + +impl NostrClient for nostr_sdk::Client { + async fn add_relays(&self, relays: Vec) -> nostr::Result<(), Error> { + self.add_relays(relays).await + } + + async fn add_relay(&self, relay: &str) -> nostr::Result { + self.add_relay(relay).await + } + + async fn connect_relay(&self, relay: &str) -> nostr::Result<(), Error> { + self.connect_relay(relay).await + } + + async fn connect(&self) { + self.connect().await + } + + async fn disconnect(&self) -> nostr::Result<(), Error> { + self.disconnect().await + } + + async fn sign_event_builder(&self, builder: EventBuilder) -> nostr::Result { + self.sign_event_builder(builder).await + } + + async fn send_event_builder(&self, builder: EventBuilder) -> nostr::Result { + self.send_event_builder(builder).await + } + + async fn send_event(&self, event: Event) -> nostr::Result { + self.send_event(event).await + } + + async fn send_event_to( + &self, + urls: Vec, + event: Event, + ) -> nostr::Result { + self.send_event_to(urls, event).await + } + + async fn send_direct_msg( + &self, + receiver: PublicKey, + msg: String, + reply_to: Option, + ) -> nostr::Result { + self.send_direct_msg(receiver, msg, reply_to).await + } + + async fn subscribe( + &self, + filters: Vec, + opts: Option, + ) -> SubscriptionId { + self.subscribe(filters, opts).await + } + + async fn get_events_of( + &self, + filters: Vec, + timeout: Option, + ) -> Result, Error> { + self.get_events_of(filters, timeout).await + } + + async fn set_signer(&self, signer: Option) { + self.set_signer(signer).await + } +} diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index f1a3eb740..ba24d22e4 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -1,5 +1,6 @@ use crate::labels::Contact; use crate::logging::MutinyLogger; +use crate::nostr::client::NostrClient; use crate::nostr::nip49::{NIP49BudgetPeriod, NIP49URI}; use crate::nostr::nwc::{ check_valid_nwc_invoice, BudgetPeriod, BudgetedSpendingConditions, NostrWalletConnect, @@ -39,6 +40,7 @@ use std::time::Duration; use std::{str::FromStr, sync::atomic::AtomicBool}; use url::Url; +mod client; pub mod nip49; pub mod nwc; pub(crate) mod primal; @@ -149,7 +151,7 @@ impl NostrKeys { /// Manages Nostr keys and has different utilities for nostr specific things #[derive(Clone)] -pub struct NostrManager { +pub struct NostrManager { /// Extended private key that is the root seed of the wallet xprivkey: ExtendedPrivKey, /// Primary key used for nostr, this will be used for signing events @@ -166,7 +168,7 @@ pub struct NostrManager { /// Atomic stop signal pub stop: Arc, /// Nostr client - pub client: Client, + pub client: C, /// Primal client pub primal_client: P, } @@ -212,7 +214,7 @@ impl Ord for NostrDiscoveredFedimint { } } -impl NostrManager { +impl NostrManager { /// Connect to the nostr relays pub async fn connect(&self) -> Result<(), MutinyError> { self.client.add_relays(self.get_relays()).await?; @@ -420,7 +422,8 @@ impl NostrManager { with_lnurl }; - let event_id = self.client.set_metadata(&with_nip05).await?; + let builder = EventBuilder::metadata(&with_nip05); + let event_id = self.client.send_event_builder(builder).await?; log_info!(self.logger, "New kind 0: {event_id}"); self.storage.set_nostr_profile(&with_nip05)?; @@ -507,7 +510,8 @@ impl NostrManager { m }; - let event_id = self.client.set_metadata(&metadata).await?; + let builder = EventBuilder::metadata(&metadata); + let event_id = self.client.send_event_builder(builder).await?; log_info!(self.logger, "New kind 0: {event_id}"); self.storage.set_nostr_profile(&metadata)?; @@ -522,7 +526,8 @@ impl NostrManager { .about("Deleted") .custom_field("deleted", true); - let event_id = self.client.set_metadata(&metadata).await?; + let builder = EventBuilder::metadata(&metadata); + let event_id = self.client.send_event_builder(builder).await?; log_info!(self.logger, "New kind 0: {event_id}"); self.storage.set_nostr_profile(&metadata)?; @@ -1039,7 +1044,7 @@ impl NostrManager { if let Some(info_event) = info_event { self.client - .send_event_to([profile.relay.as_str()], info_event) + .send_event_to(vec![profile.relay.to_string()], info_event) .await .map_err(|e| { MutinyError::Other(anyhow::anyhow!("Failed to send info event: {e:?}")) @@ -1097,7 +1102,7 @@ impl NostrManager { if let Some(nwc) = nwc { // add the relays if needed - let mut needs_connect = self.client.add_relay(&relay).await?; + let mut needs_connect = self.client.add_relay(relay.as_str()).await?; needs_connect |= self.client.add_relay(profile.relay.as_str()).await?; if needs_connect { self.client.connect().await; @@ -1190,7 +1195,7 @@ impl NostrManager { let event_id = self .client - .send_event_to([nwc.profile.relay.as_str()], response) + .send_event_to(vec![nwc.profile.relay.clone()], response) .await .map_err(|e| MutinyError::Other(anyhow::anyhow!("Failed to send info event: {e:?}")))?; @@ -2082,11 +2087,12 @@ impl NostrManager { } /// Creates a new NostrManager - pub fn from_mnemonic( + pub async fn from_mnemonic( xprivkey: ExtendedPrivKey, key_source: NostrKeySource, storage: S, primal_api: P, + client: C, logger: Arc, stop: Arc, ) -> Result { @@ -2103,7 +2109,7 @@ impl NostrManager { .map(|profile| NostrWalletConnect::new(&context, xprivkey, profile).unwrap()) .collect(); - let client = Client::new(nostr_keys.signer.clone()); + client.set_signer(Some(nostr_keys.signer.clone())).await; Ok(Self { xprivkey, @@ -2236,6 +2242,7 @@ fn is_federation_recommendation_event(event: &Event) -> bool { #[cfg(test)] mod test { use super::*; + use crate::nostr::client::MockNostrClient; use crate::nostr::primal::MockPrimalApi; use crate::storage::MemoryStorage; use crate::utils::now; @@ -2253,7 +2260,7 @@ mod test { const EXPIRED_INVOICE: &str = "lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm"; - fn create_nostr_manager() -> NostrManager { + async fn create_nostr_manager() -> NostrManager { let mnemonic = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").expect("could not generate"); let xprivkey = @@ -2265,20 +2272,26 @@ mod test { let stop = Arc::new(AtomicBool::new(false)); + #[allow(unused_mut)] // need this because of mockall + let mut client = MockNostrClient::new(); + client.expect_set_signer().return_const(()); + NostrManager::from_mnemonic( xprivkey, NostrKeySource::Derived, storage, MockPrimalApi::new(), + client, logger, stop, ) + .await .unwrap() } #[tokio::test] async fn test_process_dm() { - let nostr_manager = create_nostr_manager(); + let nostr_manager = create_nostr_manager().await; let mut inv_handler = MockInvoiceHandler::new(); inv_handler @@ -2373,7 +2386,7 @@ mod test { #[tokio::test] async fn test_create_profile() { - let nostr_manager = create_nostr_manager(); + let nostr_manager = create_nostr_manager().await; let name = "test".to_string(); @@ -2417,7 +2430,7 @@ mod test { #[tokio::test] async fn test_create_reserve_profile() { - let nostr_manager = create_nostr_manager(); + let nostr_manager = create_nostr_manager().await; let name = MUTINY_PLUS_SUBSCRIPTION_LABEL.to_string(); @@ -2533,7 +2546,7 @@ mod test { #[tokio::test] async fn test_create_nwa_profile() { - let nostr_manager = create_nostr_manager(); + let nostr_manager = create_nostr_manager().await; let name = "test nwa".to_string(); @@ -2586,7 +2599,7 @@ mod test { #[tokio::test] async fn test_edit_profile() { - let nostr_manager = create_nostr_manager(); + let nostr_manager = create_nostr_manager().await; let name = "test".to_string(); @@ -2628,7 +2641,7 @@ mod test { #[tokio::test] async fn test_delete_profile() { - let nostr_manager = create_nostr_manager(); + let nostr_manager = create_nostr_manager().await; let name = "test".to_string(); @@ -2661,7 +2674,7 @@ mod test { #[tokio::test] async fn test_deny_invoice() { - let nostr_manager = create_nostr_manager(); + let nostr_manager = create_nostr_manager().await; let name = "test".to_string(); @@ -2811,9 +2824,16 @@ mod test { assert!(is_federation_recommendation_event(&with_k)); // test nostr manager creates a valid one - - let nostr_manager = create_nostr_manager(); + #[allow(unused_mut)] // need this because of mockall + let mut nostr_manager = create_nostr_manager().await; let invite_code = InviteCode::from_str("fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er").unwrap(); + + nostr_manager + .client + .expect_sign_event_builder() + .once() + .returning(|builder| Ok(builder.to_event(&Keys::generate()).unwrap())); + let event = nostr_manager .create_recommend_federation_event(&invite_code, Network::Signet, None) .await diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index c12276708..b87b44118 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -1,5 +1,6 @@ use crate::error::MutinyError; use crate::event::HTLCStatus; +use crate::nostr::client::NostrClient; use crate::nostr::nip49::NIP49Confirmation; use crate::nostr::primal::PrimalApi; use crate::nostr::{derive_nwc_keys, NostrManager}; @@ -360,9 +361,9 @@ impl NostrWalletConnect { } } - async fn save_pending_nwc_invoice( + async fn save_pending_nwc_invoice( &self, - nostr_manager: &NostrManager, + nostr_manager: &NostrManager, event_id: EventId, event_pk: nostr::PublicKey, invoice: Bolt11Invoice, @@ -410,11 +411,11 @@ impl NostrWalletConnect { /// Handle a Nostr Wallet Connect request /// /// Returns a response event if one is needed - pub async fn handle_nwc_request( + pub async fn handle_nwc_request( &mut self, event: Event, node: &impl InvoiceHandler, - nostr_manager: &NostrManager, + nostr_manager: &NostrManager, ) -> anyhow::Result> { let client_pubkey = self.client_key.public_key(); let mut needs_save = false; @@ -722,11 +723,11 @@ impl NostrWalletConnect { Ok(Some(response)) } - async fn handle_pay_invoice_request( + async fn handle_pay_invoice_request( &mut self, event: Event, node: &impl InvoiceHandler, - nostr_manager: &NostrManager, + nostr_manager: &NostrManager, params: PayInvoiceRequestParams, needs_delete: &mut bool, needs_save: &mut bool, @@ -1487,6 +1488,7 @@ mod test { mod wasm_test { use super::*; use crate::logging::MutinyLogger; + use crate::nostr::client::MockNostrClient; use crate::nostr::primal::MockPrimalApi; use crate::nostr::{NostrKeySource, ProfileType}; use crate::storage::MemoryStorage; @@ -1515,6 +1517,12 @@ mod wasm_test { assert_eq!(pending.len(), 0); } + fn get_mock_nostr_client() -> MockNostrClient { + let mut nostr_client = MockNostrClient::new(); + nostr_client.expect_set_signer().return_const(()); + nostr_client + } + fn check_nwc_error_response(event: Event, sk: &SecretKey, expected: NIP47Error) { assert_eq!(event.kind, Kind::WalletConnectResponse); let decrypted = decrypt(sk, &event.pubkey, &event.content).unwrap(); @@ -1541,9 +1549,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), mw.logger.clone(), stop, ) + .await .unwrap(); let profile = nostr_manager @@ -1600,9 +1610,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), logger.clone(), stop, ) + .await .unwrap(); let profile = nostr_manager @@ -1807,9 +1819,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), Arc::new(MutinyLogger::default()), stop, ) + .await .unwrap(); // check we start with no pending invoices @@ -1885,9 +1899,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), mw.logger.clone(), stop, ) + .await .unwrap(); let budget = 10_000; @@ -1974,9 +1990,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), logger, stop, ) + .await .unwrap(); let budget = 10_000; @@ -2045,9 +2063,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), mw.logger.clone(), stop, ) + .await .unwrap(); let profile = nostr_manager @@ -2090,9 +2110,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), mw.logger.clone(), stop, ) + .await .unwrap(); let budget = 10_000; @@ -2142,9 +2164,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), mw.logger.clone(), stop, ) + .await .unwrap(); let budget = 10_000; @@ -2191,9 +2215,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), Arc::new(MutinyLogger::default()), stop, ) + .await .unwrap(); let best_block = BestBlock::new( @@ -2254,9 +2280,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), Arc::new(MutinyLogger::default()), stop, ) + .await .unwrap(); let amount = 69696969; @@ -2319,9 +2347,11 @@ mod wasm_test { NostrKeySource::Derived, storage.clone(), MockPrimalApi::new(), + get_mock_nostr_client(), Arc::new(MutinyLogger::default()), stop, ) + .await .unwrap(); let mut node = MockInvoiceHandler::new(); From f4184ee1ed3b579ed30ba75da324508a476b3e24 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 10 Apr 2024 14:29:32 -0500 Subject: [PATCH 3/7] Add test for discover federations --- mutiny-core/src/nostr/mod.rs | 83 +++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index ba24d22e4..9d285ac4c 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -2243,7 +2243,7 @@ fn is_federation_recommendation_event(event: &Event) -> bool { mod test { use super::*; use crate::nostr::client::MockNostrClient; - use crate::nostr::primal::MockPrimalApi; + use crate::nostr::primal::{MockPrimalApi, TrustedUser}; use crate::storage::MemoryStorage; use crate::utils::now; use crate::MockInvoiceHandler; @@ -2259,6 +2259,7 @@ mod test { use std::str::FromStr; const EXPIRED_INVOICE: &str = "lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm"; + const INVITE_CODE: &str = "fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er"; async fn create_nostr_manager() -> NostrManager { let mnemonic = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").expect("could not generate"); @@ -2826,7 +2827,7 @@ mod test { // test nostr manager creates a valid one #[allow(unused_mut)] // need this because of mockall let mut nostr_manager = create_nostr_manager().await; - let invite_code = InviteCode::from_str("fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er").unwrap(); + let invite_code = InviteCode::from_str(INVITE_CODE).unwrap(); nostr_manager .client @@ -2841,4 +2842,82 @@ mod test { assert!(is_federation_recommendation_event(&event)); } + + #[tokio::test] + async fn test_discover_federations() { + let npub = nostr::PublicKey::from_hex( + "e1ff3bfdd4e40315959b08b4fcc8245eaa514637e1d4ec2ae166b743341be1af", + ) + .unwrap(); + + // manually make NostrManager so we can use a real nostr client to get real events + let mut nostr_manager = NostrManager::from_mnemonic( + ExtendedPrivKey::new_master(Network::Bitcoin, &[]).unwrap(), + NostrKeySource::Derived, + MemoryStorage::new(None, None, None), + MockPrimalApi::new(), + Client::default(), + Arc::new(MutinyLogger::default()), + Arc::new(AtomicBool::new(false)), + ) + .await + .unwrap(); + + nostr_manager + .primal_client + .expect_get_trusted_users() + .return_once(move |_| { + Ok(vec![TrustedUser { + pubkey: npub, + trust_rating: 1.0, + metadata: Some( + Metadata::default() + .name("Ben") + .picture(Url::from_str("https://example.com").unwrap()), + ), + }]) + }); + // need to connect to relays + nostr_manager.connect().await.unwrap(); + + let federations = nostr_manager + .discover_federations(Network::Signet) + .await + .unwrap(); + + let mutinynet = federations + .iter() + .find(|f| { + f.id == FederationId::from_str( + "c8d423964c7ad944d30f57359b6e5b260e211dcfdb945140e28d4df51fd572d2", + ) + .unwrap() + }) + .unwrap(); + + // has the invite code + assert_eq!(mutinynet.invite_codes.len(), 1); + assert_eq!( + mutinynet.invite_codes.first(), + Some(&InviteCode::from_str(INVITE_CODE).unwrap()) + ); + // check we found the actual federation announcement + assert!(mutinynet.created_at.is_some()); + assert!(mutinynet.pubkey.is_some()); + assert!(mutinynet.event_id.is_some()); + assert!(mutinynet.metadata.is_some()); + + // verify we find some recommendations + assert!(!mutinynet.recommendations.is_empty()); + // find ben's recommendation + let ben = mutinynet + .recommendations + .iter() + .find(|c| c.npub.is_some_and(|n| n == npub)) + .unwrap(); + + // make sure it fills out the contact + assert!(ben.image_url.is_some()); + assert!(!ben.name.is_empty()); + } } From 668463fa50cf5f9ccdd9f917e38f55cfaba739c3 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 10 Apr 2024 14:51:59 -0500 Subject: [PATCH 4/7] Add test for creating recommendation event --- mutiny-core/src/nostr/mod.rs | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 9d285ac4c..a79632d88 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -2920,4 +2920,42 @@ mod test { assert!(ben.image_url.is_some()); assert!(!ben.name.is_empty()); } + + #[tokio::test] + async fn test_create_recommendation_event() { + let mut nostr_manager = create_nostr_manager().await; + nostr_manager + .client + .expect_sign_event_builder() + .once() + .returning(|b| Ok(b.to_event(&Keys::generate()).unwrap())); + + let invite_code = InviteCode::from_str(INVITE_CODE).unwrap(); + let event = nostr_manager + .create_recommend_federation_event(&invite_code, Network::Signet, None) + .await + .unwrap(); + + assert!(event.verify().is_ok()); + assert_eq!(event.kind, Kind::from(38000)); + assert_eq!(event.tags.len(), 4); + + // find the correct tags + assert!(event + .tags + .iter() + .any(|t| t.as_vec() == vec!["k".to_string(), "38173".to_string()])); + assert!(event + .tags + .iter() + .any(|t| t.as_vec() == vec!["d".to_string(), invite_code.federation_id().to_string()])); + assert!(event + .tags + .iter() + .any(|t| t.as_vec() == vec!["n".to_string(), "signet".to_string()])); + assert!(event + .tags + .iter() + .any(|t| t.as_vec() == vec!["u".to_string(), INVITE_CODE.to_string()])); + } } From 2001356b7bc8b864b780c29e6f778d293d726c8a Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 10 Apr 2024 15:08:05 -0500 Subject: [PATCH 5/7] Add tests for follow/unfollow --- mutiny-core/src/nostr/mod.rs | 87 ++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index a79632d88..c404853f5 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -2251,6 +2251,7 @@ mod test { use bitcoin::bip32::ExtendedPrivKey; use bitcoin::Network; use futures::executor::block_on; + use futures::try_join; use lightning::ln::PaymentSecret; use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder}; use mockall::predicate::eq; @@ -2958,4 +2959,90 @@ mod test { .iter() .any(|t| t.as_vec() == vec!["u".to_string(), INVITE_CODE.to_string()])); } + + #[tokio::test] + async fn test_follow_unfollow() { + let mut nostr_manager = create_nostr_manager().await; + nostr_manager + .client + .expect_send_event() + .returning(|e| Ok(e.id)); + + let ben = nostr::PublicKey::from_str( + "npub1u8lnhlw5usp3t9vmpz60ejpyt649z33hu82wc2hpv6m5xdqmuxhs46turz", + ) + .unwrap(); + let tony = nostr::PublicKey::from_str( + "npub1t0nyg64g5vwprva52wlcmt7fkdr07v5dr7s35raq9g0xgc0k4xcsedjgqv", + ) + .unwrap(); + + let list = nostr_manager.get_follow_list().unwrap(); + assert!(list.is_empty()); + + nostr_manager.follow_npub(ben).await.unwrap(); + let list = nostr_manager.get_follow_list().unwrap(); + assert_eq!(list.len(), 2); // follows ourselves too + + // test follow twice + nostr_manager.follow_npub(ben).await.unwrap(); + let list = nostr_manager.get_follow_list().unwrap(); + assert_eq!(list.len(), 2); + + nostr_manager.follow_npub(tony).await.unwrap(); + let list = nostr_manager.get_follow_list().unwrap(); + assert_eq!(list.len(), 3); + + nostr_manager.unfollow_npub(ben).await.unwrap(); + let list = nostr_manager.get_follow_list().unwrap(); + assert_eq!(list.len(), 2); + + nostr_manager.unfollow_npub(tony).await.unwrap(); + let list = nostr_manager.get_follow_list().unwrap(); + assert_eq!(list.len(), 1); + + // test unfollow twice + nostr_manager.unfollow_npub(tony).await.unwrap(); + let list = nostr_manager.get_follow_list().unwrap(); + assert_eq!(list.len(), 1); + } + + #[tokio::test] + async fn test_follow_concurrency() { + let mut nostr_manager = create_nostr_manager().await; + nostr_manager + .client + .expect_send_event() + .returning(|e| Ok(e.id)); + + let ben = nostr::PublicKey::from_str( + "npub1u8lnhlw5usp3t9vmpz60ejpyt649z33hu82wc2hpv6m5xdqmuxhs46turz", + ) + .unwrap(); + let tony = nostr::PublicKey::from_str( + "npub1t0nyg64g5vwprva52wlcmt7fkdr07v5dr7s35raq9g0xgc0k4xcsedjgqv", + ) + .unwrap(); + + let list = nostr_manager.get_follow_list().unwrap(); + assert!(list.is_empty()); + + try_join!( + nostr_manager.follow_npub(ben), + nostr_manager.follow_npub(tony) + ) + .unwrap(); + + let list = nostr_manager.get_follow_list().unwrap(); + assert_eq!(list.len(), 3); // follows ourselves too + + try_join!( + nostr_manager.unfollow_npub(ben), + nostr_manager.unfollow_npub(tony) + ) + .unwrap(); + + let list = nostr_manager.get_follow_list().unwrap(); + assert_eq!(list.len(), 1); + } } From 16ae251fd806ca405d5c5ee3ccc8d27af10a759e Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 10 Apr 2024 15:25:31 -0500 Subject: [PATCH 6/7] Add tests for changing nostr keys --- mutiny-core/src/nostr/mod.rs | 65 +++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index c404853f5..d07fd63f4 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -2257,6 +2257,7 @@ mod test { use mockall::predicate::eq; use nostr::prelude::rand; use nostr::prelude::rand::prelude::SliceRandom; + use nostr::SubscriptionId; use std::str::FromStr; const EXPIRED_INVOICE: &str = "lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm"; @@ -2276,7 +2277,7 @@ mod test { #[allow(unused_mut)] // need this because of mockall let mut client = MockNostrClient::new(); - client.expect_set_signer().return_const(()); + client.expect_set_signer().once().return_const(()); NostrManager::from_mnemonic( xprivkey, @@ -3045,4 +3046,66 @@ mod test { let list = nostr_manager.get_follow_list().unwrap(); assert_eq!(list.len(), 1); } + + #[tokio::test] + async fn test_change_nostr_keys() { + let mut nostr_manager = create_nostr_manager().await; + nostr_manager + .client + .expect_send_event() + .returning(|e| Ok(e.id)); + nostr_manager + .client + .expect_send_event_builder() + .returning(|e| Ok(e.to_event(&Keys::generate()).unwrap().id)); + // create follow list + nostr_manager + .follow_npub(Keys::generate().public_key()) + .await + .unwrap(); + // create a profile + nostr_manager + .primal_client + .expect_get_user_profile() + .return_once(|_| Ok(None)); + nostr_manager + .edit_profile(Some("test profile".to_string()), None, None, None) + .await + .unwrap(); + + let new_keys = Keys::generate(); + + let xprivkey = ExtendedPrivKey::new_master(Network::Bitcoin, &[0; 32]).unwrap(); + let source = NostrKeySource::Imported(new_keys.clone()); + + nostr_manager + .client + .expect_set_signer() + .once() + .return_once(|_| ()); + + nostr_manager + .client + .expect_subscribe() + .once() + .withf(|filters, opt| filters.len() == 2 && opt.is_none()) + .return_once(|_, _| SubscriptionId::generate()); + + let new_pk = nostr_manager + .change_nostr_keys(source, xprivkey) + .await + .unwrap(); + + // easy tests + assert_eq!(new_pk, new_keys.public_key()); + let npub = nostr_manager.get_npub().await; + assert_eq!(npub, new_keys.public_key()); + + // make sure we get a different follow list and profile + let list = nostr_manager.get_follow_list().unwrap(); + assert!(list.is_empty()); + let profile = nostr_manager.get_profile().unwrap(); + assert_ne!(profile.name, Some("test profile".to_string())); + assert_eq!(profile, Metadata::default()); + } } From 2d5a07886c3a2cdff276c7816614b64cf1bef61e Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 10 Apr 2024 20:18:44 -0500 Subject: [PATCH 7/7] Add tests for editing profile --- mutiny-core/src/nostr/mod.rs | 105 ++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index d07fd63f4..400ed0738 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -29,7 +29,7 @@ use nostr::prelude::{Coordinate, EventIdOrCoordinate}; use nostr::{ nips::nip04::{decrypt, encrypt}, Alphabet, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Metadata, SecretKey, - SingleLetterTag, Tag, TagKind, Timestamp, UncheckedUrl, + SingleLetterTag, Tag, TagKind, Timestamp, }; use nostr_sdk::{Client, NostrSigner, RelayPoolNotification}; use serde::{Deserialize, Serialize}; @@ -476,7 +476,17 @@ impl NostrManager { }) .collect(); let builder = EventBuilder::new(Kind::ContactList, json!(content).to_string(), tags); - self.client.send_event_builder(builder).await?; + let event = self + .nostr_keys + .read() + .await + .signer + .sign_event_builder(builder) + .await?; + + self.client.send_event(event.clone()).await?; + + update_nostr_contact_list(&self.storage, event)?; } // create real relay list @@ -484,7 +494,7 @@ impl NostrManager { let builder = EventBuilder::relay_list( RELAYS .iter() - .map(|x| (UncheckedUrl::from(x.to_string()), None)), + .map(|x| (nostr::UncheckedUrl::from(x.to_string()), None)), ); self.client.send_event_builder(builder).await?; } @@ -3108,4 +3118,93 @@ mod test { assert_ne!(profile.name, Some("test profile".to_string())); assert_eq!(profile, Metadata::default()); } + + #[tokio::test] + async fn test_profile_changes() { + let mut nostr_manager = create_nostr_manager().await; + let npub = nostr_manager.get_npub().await; + nostr_manager + .client + .expect_send_event() + .returning(|e| Ok(e.id)); + nostr_manager + .client + .expect_send_event_builder() + .returning(|e| Ok(e.to_event(&Keys::generate()).unwrap().id)); + // setup profile + nostr_manager + .primal_client + .expect_get_user_profile() + .with(eq(npub)) + .times(1) + .returning(|_| Ok(None)); + let setup = nostr_manager + .setup_new_profile(Some("test profile".to_string()), None, None, None) + .await + .unwrap(); + + let npub = nostr_manager.get_npub().await; + + // check our follow list + let list = nostr_manager.get_follow_list().unwrap(); + assert_eq!(list.len(), 1); + assert!(list.contains(&npub)); + + // check our profile + let profile = nostr_manager.get_profile().unwrap(); + assert_eq!(profile, setup); + assert_eq!(profile.name, Some("test profile".to_string())); + assert_eq!(profile.display_name, Some("test profile".to_string())); + assert_eq!(profile.lud06, None); + assert_eq!(profile.lud16, None); + assert_eq!(profile.picture, None); + assert_eq!(profile.about, None); + assert!(profile.custom.is_empty()); + + let pfp = "https://pfp.nostr.build/11670ef3e4b85e22e85a6558a7e7ea6eda960fc72f1a211042173609dce4be4e.jpg"; + let lnurl = "lnurl1dp68gurn8ghj7mrww4exctnxd9shg6npvchxxmmd9akxuatjdskkx6rpdehx2mplwdjhxumfdahr6err8ycnzef3xyunxenrvenxydf3xq6xgvekxgmrqc3cx33n2erxvc6kzce38ycnqdf5vdjr2vpevv6kvc3sv4jryenx8yuxgefex4ssq7l4mq"; + + // edit profile + nostr_manager + .primal_client + .expect_get_user_profile() + .with(eq(npub)) + .times(1) + .return_once(|_| Ok(Some(profile))); + let edited = nostr_manager + .edit_profile( + None, + Some(Url::from_str(pfp).unwrap()), + Some(LnUrl::from_str(lnurl).unwrap()), + None, + ) + .await + .unwrap(); + + // check our profile + let profile = nostr_manager.get_profile().unwrap(); + assert_eq!(profile, edited); + assert_eq!(profile.name, Some("test profile".to_string())); + assert_eq!(profile.display_name, Some("test profile".to_string())); + assert_eq!(profile.lud06, Some(lnurl.to_string())); + assert_eq!(profile.lud16, None); + assert_eq!(profile.picture, Some(pfp.to_string())); + assert_eq!(profile.about, None); + assert!(profile.custom.is_empty()); + + // delete profile + let deleted = nostr_manager.delete_profile().await.unwrap(); + + // verify it was properly deleted + let profile = nostr_manager.get_profile().unwrap(); + assert_eq!(profile, deleted); + assert_eq!(profile.name, Some("Deleted".to_string())); + assert_eq!(profile.display_name, Some("Deleted".to_string())); + assert_eq!(profile.lud06, None); + assert_eq!(profile.lud16, None); + assert_eq!(profile.picture, None); + assert_eq!(profile.about, Some("Deleted".to_string())); + assert_eq!(profile.custom.len(), 1); + assert_eq!(profile.custom.get("deleted").unwrap().as_bool(), Some(true)); + } }