diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index c5b85f374..8f65063e1 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -31,6 +31,7 @@ mod networking; mod node; pub mod nodemanager; pub mod nostr; +pub mod notifications; mod onchain; mod peermanager; pub mod redshift; @@ -54,6 +55,7 @@ use crate::labels::{Contact, LabelStorage}; use crate::nostr::nwc::{ BudgetPeriod, BudgetedSpendingConditions, NwcProfileTag, SpendingConditions, }; +use crate::notifications::MutinyNotificationClient; use crate::storage::{MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY}; use crate::{error::MutinyError, nostr::ReservedProfile}; use crate::{nodemanager::NodeManager, nostr::ProfileType}; @@ -61,6 +63,8 @@ use crate::{nostr::NostrManager, utils::sleep}; use ::nostr::key::XOnlyPublicKey; use ::nostr::{Event, Kind, Metadata}; use bip39::Mnemonic; +use bitcoin::hashes::hex::ToHex; +use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::PublicKey; use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; @@ -86,6 +90,7 @@ pub struct MutinyWalletConfig { auth_client: Option>, subscription_url: Option, scorer_url: Option, + notification_url: Option, do_not_connect_peers: bool, skip_device_lock: bool, pub safe_mode: bool, @@ -103,6 +108,7 @@ impl MutinyWalletConfig { auth_client: Option>, subscription_url: Option, scorer_url: Option, + notification_url: Option, skip_device_lock: bool, ) -> Self { Self { @@ -113,6 +119,7 @@ impl MutinyWalletConfig { user_esplora_url, user_rgs_url, scorer_url, + notification_url, lsp_url, auth_client, subscription_url, @@ -142,6 +149,7 @@ pub struct MutinyWallet { pub storage: S, pub node_manager: Arc>, pub nostr: Arc>, + pub notification_client: Option>, } impl MutinyWallet { @@ -163,10 +171,36 @@ impl MutinyWallet { NodeManager::start_sync(node_manager.clone()); + let notification_client = match config.notification_url.clone() { + Some(url) => { + let client = match config.auth_client.clone() { + Some(auth_client) => MutinyNotificationClient::new_authenticated( + auth_client, + url, + node_manager.logger.clone(), + ), + None => { + // hash key and use that as identifier + let hash = sha256::Hash::hash(&config.xprivkey.private_key.secret_bytes()); + let identifier_key = hash.to_hex(); + MutinyNotificationClient::new_unauthenticated( + url, + identifier_key, + node_manager.logger.clone(), + ) + } + }; + + Some(Arc::new(client)) + } + None => None, + }; + // create nostr manager let nostr = Arc::new(NostrManager::from_mnemonic( node_manager.xprivkey, storage.clone(), + notification_client.clone(), node_manager.logger.clone(), )?); @@ -175,6 +209,7 @@ impl MutinyWallet { storage, node_manager, nostr, + notification_client, }; #[cfg(not(test))] @@ -632,6 +667,7 @@ mod tests { None, None, None, + None, false, ); let mw = MutinyWallet::new(storage.clone(), config) @@ -662,6 +698,7 @@ mod tests { None, None, None, + None, false, ); let mut mw = MutinyWallet::new(storage.clone(), config) @@ -698,6 +735,7 @@ mod tests { None, None, None, + None, false, ); let mut mw = MutinyWallet::new(storage.clone(), config) @@ -735,6 +773,7 @@ mod tests { None, None, None, + None, false, ); let mw = MutinyWallet::new(storage.clone(), config) @@ -760,6 +799,7 @@ mod tests { None, None, None, + None, false, ); let mw2 = MutinyWallet::new(storage2.clone(), config2.clone()) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index ee7fc10c1..493b55c18 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -2692,6 +2692,7 @@ mod tests { None, None, None, + None, false, ); NodeManager::new(c, storage.clone()) @@ -2722,6 +2723,7 @@ mod tests { None, None, None, + None, false, ); let nm = NodeManager::new(c, storage) @@ -2773,6 +2775,7 @@ mod tests { None, None, None, + None, false, ); let c = c.with_safe_mode(); @@ -2809,6 +2812,7 @@ mod tests { None, None, None, + None, false, ); let nm = NodeManager::new(c, storage) diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index ddf1c0947..f7d7c9252 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -6,6 +6,7 @@ use crate::nostr::nwc::{ PendingNwcInvoice, Profile, SingleUseSpendingConditions, SpendingConditions, PENDING_NWC_EVENTS_KEY, }; +use crate::notifications::MutinyNotificationClient; use crate::storage::MutinyStorage; use crate::{error::MutinyError, utils::get_random_bip32_child_index}; use crate::{utils, HTLCStatus}; @@ -69,6 +70,8 @@ pub struct NostrManager { pub storage: S, /// Lock for pending nwc invoices pending_nwc_lock: Arc>, + /// Notification Client + pub notifications: Option>, /// Logger pub logger: Arc, } @@ -350,6 +353,17 @@ impl NostrManager { let _ = client.disconnect().await; } + // register for subscriptions + if let Some(notifications) = &self.notifications { + // just created, unwrap is safe + let uri = NostrWalletConnectURI::from_str(&profile.nwc_uri).expect("invalid uri"); + let author = uri.secret.x_only_public_key(nostr::SECP256K1).0; + + notifications + .register_nwc(author, uri.public_key, &profile.relay, &profile.name) + .await?; + } + Ok(profile) } @@ -840,6 +854,7 @@ impl NostrManager { pub fn from_mnemonic( xprivkey: ExtendedPrivKey, storage: S, + notifications: Option>, logger: Arc, ) -> Result { let context = Secp256k1::new(); @@ -862,6 +877,7 @@ impl NostrManager { nwc: Arc::new(RwLock::new(nwc)), storage, pending_nwc_lock: Arc::new(Mutex::new(())), + notifications, logger, }) } @@ -889,7 +905,7 @@ mod test { let logger = Arc::new(MutinyLogger::default()); - NostrManager::from_mnemonic(xprivkey, storage, logger).unwrap() + NostrManager::from_mnemonic(xprivkey, storage, None, logger).unwrap() } #[test] diff --git a/mutiny-core/src/notifications.rs b/mutiny-core/src/notifications.rs new file mode 100644 index 000000000..27fb67062 --- /dev/null +++ b/mutiny-core/src/notifications.rs @@ -0,0 +1,104 @@ +use crate::auth::MutinyAuthClient; +use crate::{error::MutinyError, logging::MutinyLogger}; +use anyhow::anyhow; +use lightning::util::logger::*; +use lightning::{log_error, log_info}; +use nostr::secp256k1::XOnlyPublicKey; +use reqwest::{Method, Url}; +use serde_json::{json, Value}; +use std::sync::Arc; + +#[derive(Clone)] +pub struct MutinyNotificationClient { + auth_client: Option>, + client: Option, + url: String, + id: Option, + pub logger: Arc, +} + +impl MutinyNotificationClient { + pub fn new_authenticated( + auth_client: Arc, + url: String, + logger: Arc, + ) -> Self { + log_info!(logger, "Creating authenticated notification client"); + Self { + auth_client: Some(auth_client), + client: None, + url, + id: None, // we get this from the auth client + logger, + } + } + + pub fn new_unauthenticated( + url: String, + identifier_key: String, + logger: Arc, + ) -> Self { + log_info!(logger, "Creating unauthenticated notification client"); + Self { + auth_client: None, + client: Some(reqwest::Client::new()), + url, + id: Some(identifier_key), + logger, + } + } + + async fn make_request( + &self, + method: Method, + url: Url, + body: Option, + ) -> Result { + match (self.auth_client.as_ref(), self.client.as_ref()) { + (Some(auth), _) => auth.request(method, url, body).await, + (None, Some(client)) => { + let mut request = client.request(method, url); + if let Some(body) = body { + request = request.json(&body); + } + request.send().await.map_err(|e| { + log_error!(self.logger, "Error making request: {e}"); + MutinyError::Other(anyhow!("Error making request: {e}")) + }) + } + (None, None) => unreachable!("No auth client or http client"), + } + } + + pub async fn register(&self, info: Value) -> Result<(), MutinyError> { + let url = Url::parse(&format!("{}/register", self.url)).map_err(|e| { + log_error!(self.logger, "Error parsing register url: {e}"); + MutinyError::InvalidArgumentsError + })?; + + let body = json!({"id": self.id, "info": info}); + + self.make_request(Method::POST, url, Some(body)).await?; + + Ok(()) + } + + pub async fn register_nwc( + &self, + author: XOnlyPublicKey, + tagged: XOnlyPublicKey, + relay: &str, + name: &str, + ) -> Result<(), MutinyError> { + let url = Url::parse(&format!("{}/register-nwc", self.url)).map_err(|e| { + log_error!(self.logger, "Error parsing register url: {e}"); + MutinyError::InvalidArgumentsError + })?; + + let body = json!({"id": self.id, "author": author, "tagged": tagged, "relay": relay, "name": name}); + + self.make_request(Method::POST, url, Some(body)).await?; + + Ok(()) + } +} diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 26c73e57d..2dfd1f66b 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -90,6 +90,7 @@ impl MutinyWallet { subscription_url: Option, storage_url: Option, scorer_url: Option, + notification_url: Option, do_not_connect_peers: Option, skip_device_lock: Option, safe_mode: Option, @@ -114,6 +115,7 @@ impl MutinyWallet { subscription_url, storage_url, scorer_url, + notification_url, do_not_connect_peers, skip_device_lock, safe_mode, @@ -142,6 +144,7 @@ impl MutinyWallet { subscription_url: Option, storage_url: Option, scorer_url: Option, + notification_url: Option, do_not_connect_peers: Option, skip_device_lock: Option, safe_mode: Option, @@ -235,6 +238,7 @@ impl MutinyWallet { auth_client, subscription_url, scorer_url, + notification_url, skip_device_lock.unwrap_or(false), ); @@ -1431,6 +1435,22 @@ impl MutinyWallet { Ok(self.inner.reset_onchain_tracker().await?) } + /// Register the wallet for web-push notifications + #[wasm_bindgen] + pub async fn register_web_push(&self, info: JsValue) -> Result<(), MutinyJsError> { + match self.inner.notification_client.as_ref() { + Some(client) => { + let info = info.into_serde()?; + client.register(info).await?; + } + None => return Err(MutinyJsError::NotFound), + } + + log::info!("Web Push Registered!"); + + Ok(()) + } + /// Exports the current state of the node manager to a json object. #[wasm_bindgen] pub async fn export_json(password: Option) -> Result { @@ -1561,6 +1581,7 @@ mod tests { None, None, None, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1594,6 +1615,7 @@ mod tests { None, None, None, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1616,6 +1638,7 @@ mod tests { None, None, None, + None, ) .await; @@ -1654,6 +1677,7 @@ mod tests { None, None, None, + None, ) .await .unwrap(); @@ -1692,6 +1716,7 @@ mod tests { None, None, None, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1758,6 +1783,7 @@ mod tests { None, None, None, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1812,6 +1838,7 @@ mod tests { None, None, None, + None, ) .await .expect("mutiny wallet should initialize");