diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 92ff07534..a5386ca9e 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -37,7 +37,7 @@ pub mod redshift; pub mod scb; pub mod scorer; pub mod storage; -mod subscription; +pub mod subscription; pub mod vss; #[cfg(any(test, feature = "test-utils"))] @@ -55,6 +55,7 @@ use crate::nostr::nwc::{ BudgetPeriod, BudgetedSpendingConditions, NwcProfileTag, SpendingConditions, }; use crate::storage::{MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY}; +use crate::subscription::MutinySubscriptionClient; use crate::{error::MutinyError, nostr::ReservedProfile}; use crate::{nodemanager::NodeManager, nostr::ProfileType}; use crate::{nostr::NostrManager, utils::sleep}; @@ -84,7 +85,7 @@ pub struct MutinyWalletConfig { user_rgs_url: Option, lsp_url: Option, auth_client: Option>, - subscription_url: Option, + subscription_client: Option>, scorer_url: Option, do_not_connect_peers: bool, skip_device_lock: bool, @@ -101,7 +102,7 @@ impl MutinyWalletConfig { user_rgs_url: Option, lsp_url: Option, auth_client: Option>, - subscription_url: Option, + subscription_client: Option>, scorer_url: Option, skip_device_lock: bool, ) -> Self { @@ -115,7 +116,7 @@ impl MutinyWalletConfig { scorer_url, lsp_url, auth_client, - subscription_url, + subscription_client, do_not_connect_peers: false, skip_device_lock, safe_mode: false, @@ -179,6 +180,13 @@ impl MutinyWallet { nostr, }; + // if we have a subscription, ensure we have the mutiny subscription profile + if mw.storage.premium() { + if let Some(ref sub) = mw.node_manager.subscription_client { + mw.ensure_mutiny_nwc_profile(sub, false).await?; + } + } + #[cfg(not(test))] { // if we need a full sync from a restore @@ -362,7 +370,7 @@ impl MutinyWallet { // account for 3 day grace period if expired_time + 86_400 * 3 > crate::utils::now().as_secs() { // now submit the NWC string if never created before - self.ensure_mutiny_nwc_profile(subscription_client, false) + self.ensure_mutiny_nwc_profile(subscription_client.as_ref(), false) .await?; } } @@ -398,7 +406,7 @@ impl MutinyWallet { .await?; // now submit the NWC string if never created before - self.ensure_mutiny_nwc_profile(subscription_client, autopay) + self.ensure_mutiny_nwc_profile(subscription_client.as_ref(), autopay) .await?; Ok(()) @@ -407,9 +415,9 @@ impl MutinyWallet { } } - async fn ensure_mutiny_nwc_profile( + pub(crate) async fn ensure_mutiny_nwc_profile( &self, - subscription_client: Arc, + subscription_client: &MutinySubscriptionClient, autopay: bool, ) -> Result<(), MutinyError> { let nwc_profiles = self.nostr.profiles(); diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 8cc0fa589..cee6ad097 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -765,21 +765,10 @@ impl NodeManager { .expect("failed to make lnurl client"), ); - let (subscription_client, auth) = if let Some(auth_client) = c.auth_client { - if let Some(subscription_url) = c.subscription_url { - let auth = auth_client.auth.clone(); - let s = Arc::new(MutinySubscriptionClient::new( - auth_client, - subscription_url, - logger.clone(), - )); - (Some(s), auth) - } else { - (None, auth_client.auth.clone()) - } + let auth = if let Some(auth) = c.auth_client { + auth.auth.clone() } else { - let auth_manager = AuthManager::new(c.xprivkey)?; - (None, auth_manager) + AuthManager::new(c.xprivkey)? }; let price_cache = storage @@ -807,7 +796,7 @@ impl NodeManager { auth, lnurl_client, lsp_clients, - subscription_client, + subscription_client: c.subscription_client, logger, bitcoin_price_cache: Arc::new(Mutex::new(price_cache)), do_not_connect_peers: c.do_not_connect_peers, diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 83fc387e7..5ae73e097 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -101,6 +101,11 @@ pub trait MutinyStorage: Clone + Sized + 'static { /// Get the VSS client used for storage fn vss_client(&self) -> Option>; + /// If the VSS client has premium features enabledss + fn premium(&self) -> bool { + self.vss_client().is_some_and(|v| v.premium) + } + /// Set a value in the storage, the value will already be encrypted if needed fn set(&self, key: impl AsRef, value: T) -> Result<(), MutinyError> where diff --git a/mutiny-core/src/subscription.rs b/mutiny-core/src/subscription.rs index 7c6f4f8e4..a1f482e1b 100644 --- a/mutiny-core/src/subscription.rs +++ b/mutiny-core/src/subscription.rs @@ -7,18 +7,14 @@ use serde::{Deserialize, Serialize}; use crate::{auth::MutinyAuthClient, error::MutinyError, logging::MutinyLogger, nodemanager::Plan}; -pub(crate) struct MutinySubscriptionClient { +pub struct MutinySubscriptionClient { auth_client: Arc, url: String, logger: Arc, } impl MutinySubscriptionClient { - pub(crate) fn new( - auth_client: Arc, - url: String, - logger: Arc, - ) -> Self { + pub fn new(auth_client: Arc, url: String, logger: Arc) -> Self { Self { auth_client, url, @@ -26,6 +22,12 @@ impl MutinySubscriptionClient { } } + /// Checks whether or not the user is subscribed to Mutiny+. + /// Submits a NWC string to keep the subscription active if not expired. + /// + /// Returns None if there's no subscription at all. + /// Returns Some(u64) for their unix expiration timestamp, which may be in the + /// past or in the future, depending on whether or not it is currently active. pub async fn check_subscribed(&self) -> Result, MutinyError> { let url = Url::parse(&format!("{}/v1/check-subscribed", self.url)).map_err(|e| { log_error!(self.logger, "Error parsing check subscribed url: {e}"); diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index 1c9dac6be..7641993b5 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -17,6 +17,7 @@ pub struct MutinyVssClient { url: String, store_id: Option, encryption_key: SecretKey, + pub premium: bool, pub logger: Arc, } @@ -88,6 +89,7 @@ impl MutinyVssClient { url, store_id: None, // we get this from the auth client encryption_key, + premium: false, // set later logger, } } @@ -105,6 +107,7 @@ impl MutinyVssClient { url, store_id: Some(pk), encryption_key, + premium: true, // unauthenticated clients are self-hosted so they are premium logger, } } diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index cbc86eb0c..960a2aa30 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -24,7 +24,9 @@ use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{Address, Network, OutPoint, Transaction, Txid}; use futures::lock::Mutex; use gloo_utils::format::JsValueSerdeExt; +use lightning::log_info; use lightning::routing::gossip::NodeId; +use lightning::util::logger::Logger; use lightning_invoice::Bolt11Invoice; use lnurl::lnurl::LnUrl; use mutiny_core::auth::MutinyAuthClient; @@ -35,7 +37,8 @@ use mutiny_core::redshift::RedshiftManager; use mutiny_core::redshift::RedshiftRecipient; use mutiny_core::scb::EncryptedSCB; use mutiny_core::storage::MutinyStorage; -use mutiny_core::utils::sleep; +use mutiny_core::subscription::MutinySubscriptionClient; +use mutiny_core::utils::{now, sleep}; use mutiny_core::vss::MutinyVssClient; use mutiny_core::{labels::LabelStorage, nodemanager::NodeManager}; use mutiny_core::{logging::MutinyLogger, nostr::ProfileType}; @@ -168,7 +171,7 @@ impl MutinyWallet { let seed = mnemonic.to_seed(""); let xprivkey = ExtendedPrivKey::new_master(network, &seed).unwrap(); - let (auth_client, vss_client) = if safe_mode { + let (auth_client, mut vss_client) = if safe_mode { (None, None) } else if let Some(auth_url) = auth_url.clone() { let auth_manager = AuthManager::new(xprivkey).unwrap(); @@ -187,28 +190,53 @@ impl MutinyWallet { )); let vss = storage_url.map(|url| { - Arc::new(MutinyVssClient::new_authenticated( + MutinyVssClient::new_authenticated( auth_client.clone(), url, xprivkey.private_key, logger.clone(), - )) + ) }); (Some(auth_client), vss) } else { let vss = storage_url.map(|url| { - Arc::new(MutinyVssClient::new_unauthenticated( - url, - xprivkey.private_key, - logger.clone(), - )) + MutinyVssClient::new_unauthenticated(url, xprivkey.private_key, logger.clone()) }); (None, vss) }; - let storage = IndexedDbStorage::new(password, cipher, vss_client, logger.clone()).await?; + let (subscription_client, subscribed) = if let Some(ref auth_client) = auth_client { + if let Some(subscription_url) = subscription_url { + let client = Arc::new(MutinySubscriptionClient::new( + auth_client.clone(), + subscription_url, + logger.clone(), + )); + + // check if we're subscribed, add 3 day grace period + let sub_expired_time = client.check_subscribed().await?.unwrap_or_default(); + let subscribed = sub_expired_time + 86_400 * 3 > now().as_secs(); + + (Some(client), subscribed) + } else { + (None, false) + } + } else { + (None, false) + }; + + if subscribed { + log_info!(logger, "Welcome back Mutiny+ user!"); + if let Some(vss) = vss_client.as_mut() { + vss.premium = true; + } + } + + let storage = + IndexedDbStorage::new(password, cipher, vss_client.map(Arc::new), logger.clone()) + .await?; let mut config = mutiny_core::MutinyWalletConfig::new( xprivkey, @@ -218,7 +246,7 @@ impl MutinyWallet { user_rgs_url, lsp_url, auth_client, - subscription_url, + subscription_client, scorer_url, skip_device_lock.unwrap_or(false), ); @@ -1382,14 +1410,9 @@ impl MutinyWallet { } /// Checks whether or not the user is subscribed to Mutiny+. - /// Submits a NWC string to keep the subscription active if not expired. - /// - /// Returns None if there's no subscription at all. - /// Returns Some(u64) for their unix expiration timestamp, which may be in the - /// past or in the future, depending on whether or not it is currently active. #[wasm_bindgen] - pub async fn check_subscribed(&self) -> Result, MutinyJsError> { - Ok(self.inner.check_subscribed().await?) + pub fn check_subscribed(&self) -> bool { + self.inner.storage.premium() } /// Gets the subscription plans for Mutiny+ subscriptions