diff --git a/docs/Testing.md b/docs/Testing.md index 50f57dccc..75c29b6a8 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -6,6 +6,7 @@ Things to test before cutting a release: - [ ] Enable diagnostics - [ ] Join a fedimint - [ ] Leave a fedimint and rejoin +- [ ] Create self custodial lightning - [ ] Receive on lightning - [ ] One with fedimint - [ ] One that creates a channel @@ -18,6 +19,8 @@ Things to test before cutting a release: - [ ] Send on chain - [ ] Swap to lightning - [ ] Swap fedimint to lightning + - [ ] without self custodial initated + - [ ] with self custodial created - [ ] Nostr Wallet Connect - [ ] Auto approval - [ ] Manual approval @@ -34,6 +37,7 @@ Things to test before cutting a release: - [ ] Mutual Close Channel - [ ] Known Issue: balance will be double counted until 6 confirmations - [ ] Force Close Channel +- [ ] Wallet with no balances or fedimint takes you to add fedimint screen - [ ] Get Mutiny+ - [ ] Test lightning address payments - [ ] Change to Zeus LSP (https://mutinynet-flow.lnolymp.us) diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index 1f33e3bee..3e2113c73 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -179,6 +179,9 @@ pub enum MutinyError { /// Failed to connect to a federation. #[error("Failed to connect to a federation.")] FederationConnectionFailed, + /// A node manager has not been created yet. + #[error("A node manager has not been created yet.")] + NodeManagerRequired, #[error(transparent)] Other(anyhow::Error), } @@ -263,6 +266,7 @@ impl PartialEq for MutinyError { (Self::TokenAlreadySpent, Self::TokenAlreadySpent) => true, (Self::FederationRequired, Self::FederationRequired) => true, (Self::FederationConnectionFailed, Self::FederationConnectionFailed) => true, + (Self::NodeManagerRequired, Self::NodeManagerRequired) => true, (Self::Other(e), Self::Other(e2)) => e.to_string() == e2.to_string(), _ => false, } diff --git a/mutiny-core/src/labels.rs b/mutiny-core/src/labels.rs index 493924e21..15026702f 100644 --- a/mutiny-core/src/labels.rs +++ b/mutiny-core/src/labels.rs @@ -1,6 +1,6 @@ use crate::error::MutinyError; -use crate::nodemanager::NodeManager; use crate::storage::MutinyStorage; +use crate::MutinyWallet; use bitcoin::Address; use lightning_invoice::Bolt11Invoice; use lnurl::lightning_address::LightningAddress; @@ -445,7 +445,7 @@ impl LabelStorage for S { } } -impl LabelStorage for NodeManager { +impl LabelStorage for MutinyWallet { fn get_address_labels(&self) -> Result>, MutinyError> { self.storage.get_address_labels() } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 2f1ca827a..2dda7e84f 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -96,9 +96,10 @@ use ::nostr::prelude::ZapRequestData; use ::nostr::Tag; use ::nostr::{EventBuilder, EventId, HttpMethod, JsonUtil, Keys, Kind}; use async_lock::RwLock; -use bdk_chain::ConfirmationTime; +use bdk_chain::{BlockId, ConfirmationTime}; use bip39::Mnemonic; pub use bitcoin; +use bitcoin::address::NetworkUnchecked; use bitcoin::secp256k1::{PublicKey, ThirtyTwoByteHash}; use bitcoin::{bip32::ExtendedPrivKey, Transaction}; use bitcoin::{hashes::sha256, Network, Txid}; @@ -141,6 +142,7 @@ use uuid::Uuid; use web_time::Instant; use crate::labels::LabelItem; +use crate::nodemanager::NodeIdentity; use crate::nostr::{NostrKeySource, RELAYS}; #[cfg(test)] use mockall::{automock, predicate::*}; @@ -944,22 +946,30 @@ impl MutinyWalletBuilder { log_trace!(logger, "setting up node manager"); let start = Instant::now(); - let mut nm_builder = NodeManagerBuilder::new(self.xprivkey, self.storage.clone()) - .with_config(config.clone()); - nm_builder.with_logger(logger.clone()); - nm_builder.with_esplora(esplora.clone()); - let node_manager = Arc::new(nm_builder.build().await?); - log_trace!( - logger, - "NodeManager started, took: {}ms", - start.elapsed().as_millis() - ); + let node_manager = if self.storage.get_nodes()?.is_empty() { + Arc::new(RwLock::new(None)) + } else { + let mut nm_builder = NodeManagerBuilder::new(self.xprivkey, self.storage.clone()) + .with_config(config.clone()); + nm_builder.with_logger(logger.clone()); + nm_builder.with_esplora(esplora.clone()); + + let nm = nm_builder.build().await?; + + let node_manager = Arc::new(RwLock::new(Some(nm))); + log_trace!(logger, "starting node manager sync"); + // start sync + NodeManager::start_sync(node_manager.clone(), network, stop.clone(), logger.clone()); + log_trace!(logger, "finished node manager sync"); - // start syncing node manager - log_trace!(logger, "starting node manager sync"); - NodeManager::start_sync(node_manager.clone()); - log_trace!(logger, "finished node manager sync"); + log_trace!( + logger, + "NodeManager started, took: {}ms", + start.elapsed().as_millis() + ); + node_manager + }; log_trace!(logger, "creating primal client"); let primal_client = PrimalClient::new( @@ -1110,18 +1120,27 @@ impl MutinyWalletBuilder { // populate the activity index log_trace!(logger, "populating activity index"); - let mut activity_index = node_manager - .wallet - .list_transactions(false)? - .into_iter() - .map(|t| IndexItem { - timestamp: match t.confirmation_time { - ConfirmationTime::Confirmed { time, .. } => Some(time), - ConfirmationTime::Unconfirmed { .. } => None, - }, - key: format!("{ONCHAIN_PREFIX}{}", t.internal_id), - }) - .collect::>(); + let mut activity_index = { + node_manager + .read() + .await + .as_ref() + .map(|nm| { + nm.wallet + .list_transactions(false) + .unwrap() + .into_iter() + .map(|t| IndexItem { + timestamp: match t.confirmation_time { + ConfirmationTime::Confirmed { time, .. } => Some(time), + ConfirmationTime::Unconfirmed { .. } => None, + }, + key: format!("{ONCHAIN_PREFIX}{}", t.internal_id), + }) + .collect::>() + }) + .unwrap_or_default() + }; // add any transaction details stored from fedimint let transaction_details = self @@ -1223,20 +1242,6 @@ impl MutinyWalletBuilder { return Ok(mw); } - // if we don't have any nodes, create one - log_trace!(logger, "listing nodes"); - if mw.node_manager.list_nodes().await?.is_empty() { - log_trace!(logger, "going to create first node"); - let nm = mw.node_manager.clone(); - // spawn in background, this can take a while and we don't want to block - utils::spawn(async move { - if let Err(e) = nm.new_node().await { - log_error!(nm.logger, "Failed to create first node: {e}"); - } - }) - }; - log_trace!(logger, "finished listing nodes"); - // start the nostr background process log_trace!(logger, "starting nostr"); mw.start_nostr().await; @@ -1284,7 +1289,7 @@ pub struct MutinyWallet { xprivkey: ExtendedPrivKey, config: MutinyWalletConfig, pub(crate) storage: S, - pub node_manager: Arc>, + pub node_manager: Arc>>>, pub nostr: Arc>, pub federation_storage: Arc>, pub(crate) federations: Arc>>>>, @@ -1305,19 +1310,33 @@ pub struct MutinyWallet { impl MutinyWallet { /// Starts up all the nodes again. - /// Not needed after [NodeManager]'s `new()` function. + /// Not needed after [MutinyWallet]'s `new()` function. pub async fn start(&mut self) -> Result<(), MutinyError> { log_trace!(self.logger, "calling start"); self.storage.start().await?; + self.stop.store(false, Ordering::Relaxed); + + if self.storage.get_nodes()?.is_empty() { + return Ok(()); + } let mut nm_builder = NodeManagerBuilder::new(self.xprivkey, self.storage.clone()) .with_config(self.config.clone()); nm_builder.with_logger(self.logger.clone()); + let node_manager = nm_builder.build().await?; - // when we restart, gen a new session id - self.node_manager = Arc::new(nm_builder.build().await?); - NodeManager::start_sync(self.node_manager.clone()); + // replace the node manager + let mut lock = self.node_manager.write().await; + *lock = Some(node_manager); + + // start sync + NodeManager::start_sync( + self.node_manager.clone(), + self.network, + self.stop.clone(), + self.logger.clone(), + ); log_trace!(self.logger, "finished calling start"); Ok(()) @@ -1500,6 +1519,106 @@ impl MutinyWallet { log_trace!(self.logger, "finished calling start_nostr"); } + /// Creates a node manager with a node and returns the node identity. + /// If a node manager already exists, it will return an error. + pub async fn create_node_manager(&self) -> Result { + log_trace!(self.logger, "calling create_node_manager"); + + if self.is_safe_mode() { + return Err(MutinyError::NotRunning); + } + + let mut lock = self.node_manager.write().await; + // if we already have a node manager, return + if let Some(node_manager) = lock.as_ref() { + // if we don't have a node, we should unarchive the first node + // otherwise, we already have a functional node manager + let nodes = node_manager.list_nodes().await?; + if nodes.is_empty() { + return node_manager.unarchive_first_node().await; + } else { + return Err(MutinyError::AlreadyRunning); + } + } + + let mut nm_builder = NodeManagerBuilder::new(self.xprivkey, self.storage.clone()) + .with_config(self.config.clone()); + nm_builder.with_logger(self.logger.clone()); + let node_manager = nm_builder.build().await?; + + // create first node + let node_identity = node_manager.create_first_node().await?; + + // replace the node manager + *lock = Some(node_manager); + + // start sync + NodeManager::start_sync( + self.node_manager.clone(), + self.network, + self.stop.clone(), + self.logger.clone(), + ); + + log_trace!(self.logger, "finished calling create_node_manager"); + + Ok(node_identity) + } + + /// Creates a node manager if we don't have one already. + pub async fn create_node_manager_if_needed(&self) -> Result, MutinyError> { + match self.create_node_manager().await { + Ok(ident) => Ok(Some(ident)), + Err(MutinyError::AlreadyRunning) => Ok(None), // this means we already have a node manager + Err(e) => Err(e), + } + } + + /// Removes the node manager from the wallet. If the node manager has any balances, + /// this function will fail. + pub async fn remove_node_manager(&self) -> Result<(), MutinyError> { + log_trace!(self.logger, "calling remove_node_manager"); + if self.is_safe_mode() { + return Err(MutinyError::NotRunning); + } + + let mut lock = self.node_manager.write().await; + // if we don't have a node manager, return + if lock.is_none() { + return Ok(()); + } + + let node_manager = lock.as_ref().expect("should have a node manager"); + + // if the node manager is still in use, return an error + if !node_manager.is_unused().await? { + return Err(MutinyError::InvalidArgumentsError); + } + + // archive all nodes + // this will verify that each node has no balance and every channel monitor is + // safe to be pruned. + let nodes = node_manager.list_nodes().await?; + for pk in nodes { + node_manager.archive_node(pk).await?; + } + + // save node storage + { + let mut node_storage = node_manager.node_storage.read().await.clone(); + node_storage.version += 1; + self.storage.insert_nodes(&node_storage).await?; + } + + node_manager.stop().await?; + + *lock = None; + + log_trace!(self.logger, "finished calling remove_node_manager"); + + Ok(()) + } + /// Pays a lightning invoice from a federation (preferred) or node. /// An amount should only be provided if the invoice does not have an amount. /// Amountless invoices cannot be paid by a federation. @@ -1586,33 +1705,35 @@ impl MutinyWallet { // If any balance at all, then fallback to node manager for payment. // Take the error from the node manager as the priority. - let res = if self - .node_manager - .nodes - .read() - .await - .iter() - .flat_map(|(_, n)| n.channel_manager.list_channels()) - .map(|c| c.balance_msat) - .sum::() - > 0 - { - let res = self - .node_manager - .pay_invoice(None, inv, amt_sats, labels) - .await?; + let res = if let Some(node_manager) = self.node_manager.read().await.as_ref() { + if node_manager + .nodes + .read() + .await + .iter() + .flat_map(|(_, n)| n.channel_manager.list_channels()) + .map(|c| c.balance_msat) + .sum::() + > 0 + { + let res = node_manager + .pay_invoice(None, inv, amt_sats, labels) + .await?; - // spawn a task to remove the pending invoice if it exists - let nostr_clone = self.nostr.clone(); - let payment_hash = *inv.payment_hash(); - let logger = self.logger.clone(); - utils::spawn(async move { - if let Err(e) = nostr_clone.remove_pending_nwc_invoice(&payment_hash).await { - log_warn!(logger, "Failed to remove pending NWC invoice: {e}"); - } - }); + // spawn a task to remove the pending invoice if it exists + let nostr_clone = self.nostr.clone(); + let payment_hash = *inv.payment_hash(); + let logger = self.logger.clone(); + utils::spawn(async move { + if let Err(e) = nostr_clone.remove_pending_nwc_invoice(&payment_hash).await { + log_warn!(logger, "Failed to remove pending NWC invoice: {e}"); + } + }); - Ok(res) + Ok(res) + } else { + Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance)) + } } else { Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance)) }; @@ -1841,6 +1962,10 @@ impl MutinyWallet { (invoice, 0) } else { self.node_manager + .read() + .await + .as_ref() + .ok_or(MutinyError::NodeManagerRequired)? .create_invoice(amt, vec![SWAP_LABEL.to_string()]) .await? }; @@ -1869,6 +1994,10 @@ impl MutinyWallet { (invoice, 0) } else { self.node_manager + .read() + .await + .as_ref() + .ok_or(MutinyError::NodeManagerRequired)? .create_invoice(amt, vec![SWAP_LABEL.to_string()]) .await? }; @@ -1926,13 +2055,16 @@ impl MutinyWallet { // If any balance at all, then fallback to node manager for payment. // Take the error from the node manager as the priority. - let b = self.node_manager.get_balance().await?; - let res = if b.confirmed + b.unconfirmed > 0 { - let res = self - .node_manager - .send_to_address(send_to, amount, labels, fee_rate) - .await?; - Ok(res) + let res = if let Some(node_manager) = self.node_manager.read().await.as_ref() { + let b = node_manager.get_balance().await?; + if b.confirmed + b.unconfirmed > 0 { + let res = node_manager + .send_to_address(send_to, amount, labels, fee_rate) + .await?; + Ok(res) + } else { + Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance)) + } } else { Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance)) }; @@ -1981,13 +2113,15 @@ impl MutinyWallet { // If federation client is not found, continue to next federation } - let b = self.node_manager.get_balance().await?; - let res = if b.confirmed + b.unconfirmed > 0 { - let res = self - .node_manager - .estimate_tx_fee(destination_address, amount, fee_rate)?; + let res = if let Some(node_manager) = self.node_manager.read().await.as_ref() { + let b = node_manager.get_balance().await?; + if b.confirmed + b.unconfirmed > 0 { + let res = node_manager.estimate_tx_fee(destination_address, amount, fee_rate)?; - Ok(res) + Ok(res) + } else { + Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance)) + } } else { Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance)) }; @@ -2028,15 +2162,17 @@ impl MutinyWallet { // If federation client is not found, continue to next federation } - let b = self.node_manager.get_balance().await?; - let res = if b.confirmed + b.unconfirmed > 0 { - let res = self - .node_manager - .estimate_sweep_tx_fee(destination_address, fee_rate)?; + let res = if let Some(node_manager) = self.node_manager.read().await.as_ref() { + let b = node_manager.get_balance().await?; + if b.confirmed + b.unconfirmed > 0 { + let res = node_manager.estimate_sweep_tx_fee(destination_address, fee_rate)?; - Ok(res) + Ok(res) + } else { + log_error!(self.logger, "node manager doesn't have a balance"); + Err(MutinyError::InsufficientBalance) + } } else { - log_error!(self.logger, "node manager doesn't have a balance"); Err(MutinyError::InsufficientBalance) }; log_trace!(self.logger, "calling estimate_sweep_tx_fee"); @@ -2085,16 +2221,19 @@ impl MutinyWallet { // If federation client is not found, continue to next federation } - let b = self.node_manager.get_balance().await?; - let res = if b.confirmed + b.unconfirmed > 0 { - let res = self - .node_manager - .sweep_wallet(send_to.clone(), labels, fee_rate) - .await?; + let res = if let Some(node_manager) = self.node_manager.read().await.as_ref() { + let b = node_manager.get_balance().await?; + if b.confirmed + b.unconfirmed > 0 { + let res = node_manager + .sweep_wallet(send_to.clone(), labels, fee_rate) + .await?; - Ok(res) + Ok(res) + } else { + log_error!(self.logger, "node manager doesn't have a balance"); + Err(MutinyError::InsufficientBalance) + } } else { - log_error!(self.logger, "node manager doesn't have a balance"); Err(MutinyError::InsufficientBalance) }; log_trace!(self.logger, "finished calling sweep_wallet"); @@ -2123,12 +2262,13 @@ impl MutinyWallet { } // Fallback to node_manager address creation - let Ok(addr) = self.node_manager.get_new_address(labels.clone()) else { - return Err(MutinyError::WalletOperationFailed); - }; - - log_trace!(self.logger, "finished calling create_address"); - Ok(addr) + if let Some(node_manager) = self.node_manager.read().await.as_ref() { + let addr = node_manager.get_new_address(labels.clone())?; + log_trace!(self.logger, "finished calling create_address"); + Ok(addr) + } else { + Err(MutinyError::NodeManagerRequired) + } } async fn create_lightning_invoice( @@ -2154,10 +2294,15 @@ impl MutinyWallet { } // Fallback to node_manager invoice creation if no federation invoice created - let (inv, _fee) = self.node_manager.create_invoice(amount, labels).await?; - - log_trace!(self.logger, "finished calling create_lightning_invoice"); - Ok(inv) + let node_manager = self.node_manager.read().await; + match node_manager.as_ref() { + Some(nm) => { + let (inv, _) = nm.create_invoice(amount, labels).await?; + log_trace!(self.logger, "finished calling create_lightning_invoice"); + Ok(inv) + } + None => Err(MutinyError::NodeManagerRequired), + } } /// Gets the current balance of the wallet. @@ -2167,7 +2312,10 @@ impl MutinyWallet { pub async fn get_balance(&self) -> Result { log_trace!(self.logger, "calling get_balance"); - let ln_balance = self.node_manager.get_balance().await?; + let ln_balance: NodeBalance = match self.node_manager.read().await.as_ref() { + Some(nm) => nm.get_balance().await?, + None => Default::default(), + }; let federation_balance = self.get_total_federation_balance().await?; log_trace!(self.logger, "finished calling get_balance"); @@ -2199,7 +2347,7 @@ impl MutinyWallet { } /// Get the sorted activity list for lightning payments, channels, and txs. - pub fn get_activity( + pub async fn get_activity( &self, limit: Option, offset: Option, @@ -2266,10 +2414,13 @@ impl MutinyWallet { // convert keys to txid let txid_str = item.key.trim_start_matches(ONCHAIN_PREFIX); let txid: Txid = Txid::from_str(txid_str)?; - if let Some(tx_details) = self.node_manager.get_transaction(txid)? { - // make sure it is a relevant transaction - if tx_details.sent != 0 || tx_details.received != 0 { - activities.push(ActivityItem::OnChain(tx_details)); + + if let Some(node_manager) = self.node_manager.read().await.as_ref() { + if let Some(tx_details) = node_manager.get_transaction(txid)? { + // make sure it is a relevant transaction + if tx_details.sent != 0 || tx_details.received != 0 { + activities.push(ActivityItem::OnChain(tx_details)); + } } } } else if item.key.starts_with(TRANSACTION_DETAILS_PREFIX_KEY) { @@ -2291,7 +2442,10 @@ impl MutinyWallet { Ok(activities) } - pub fn get_transaction(&self, txid: Txid) -> Result, MutinyError> { + pub async fn get_transaction( + &self, + txid: Txid, + ) -> Result, MutinyError> { log_trace!(self.logger, "calling get_transaction"); // check our local cache/state for fedimint first @@ -2299,7 +2453,10 @@ impl MutinyWallet { Some(t) => Ok(Some(t)), None => { // fall back to node manager - self.node_manager.get_transaction(txid) + match self.node_manager.read().await.as_ref() { + Some(node_manager) => node_manager.get_transaction(txid), + None => Ok(None), + } } }; log_trace!(self.logger, "finished calling get_transaction"); @@ -2307,6 +2464,89 @@ impl MutinyWallet { res } + /// Checks if the given address has any transactions. + /// If it does, it returns the details of the first transaction. + /// + /// This should be used to check if a payment has been made to an address. + pub async fn check_address( + &self, + address: Address, + ) -> Result, MutinyError> { + log_trace!(self.logger, "calling check_address"); + let address = address.require_network(self.network)?; + + let script = address.payload.script_pubkey(); + let txs = self.esplora.scripthash_txs(&script, None).await?; + + let details_opt = txs.first().map(|tx| { + let received: u64 = tx + .vout + .iter() + .filter(|v| v.scriptpubkey == script) + .map(|v| v.value) + .sum(); + + let confirmation_time = tx + .confirmation_time() + .map(|c| ConfirmationTime::Confirmed { + height: c.height, + time: c.timestamp, + }) + .unwrap_or(ConfirmationTime::Unconfirmed { + last_seen: utils::now().as_secs(), + }); + + let address_labels = self.storage.get_address_labels().unwrap_or_default(); + let labels = address_labels + .get(&address.to_string()) + .cloned() + .unwrap_or_default(); + + let details = TransactionDetails { + transaction: Some(tx.to_tx()), + txid: Some(tx.txid), + internal_id: tx.txid, + received, + sent: 0, + fee: None, + confirmation_time, + labels, + }; + + let block_id = match tx.status.block_hash { + Some(hash) => { + let height = tx + .status + .block_height + .expect("block height must be present"); + Some(BlockId { hash, height }) + } + None => None, + }; + + (details, block_id) + }); + + // if we found a tx we should try to import it into the wallet + if let Some((details, block_id)) = details_opt.clone() { + let node_manager = self.node_manager.clone(); + utils::spawn(async move { + if let Some(nm) = node_manager.read().await.as_ref() { + let wallet = nm.wallet.clone(); + let tx = details.transaction.expect("tx must be present"); + wallet + .insert_tx(tx, details.confirmation_time, block_id) + .await + .expect("failed to insert tx"); + } + }); + } + + log_trace!(self.logger, "finished calling check_address"); + + Ok(details_opt.map(|(d, _)| d)) + } + /// Returns all the lightning activity for a given label pub async fn get_label_activity( &self, @@ -2314,7 +2554,7 @@ impl MutinyWallet { ) -> Result, MutinyError> { log_trace!(self.logger, "calling get_label_activity"); - let Some(label_item) = self.node_manager.get_label(label)? else { + let Some(label_item) = self.storage.get_label(label)? else { return Ok(Vec::new()); }; @@ -2833,7 +3073,9 @@ impl MutinyWallet { self.stop.store(true, Ordering::Relaxed); - self.node_manager.stop().await?; + if let Some(nm) = self.node_manager.read().await.as_ref() { + nm.stop().await?; + } // stop the indexeddb object to close db connection if self.storage.connected().unwrap_or(false) { @@ -2888,10 +3130,13 @@ impl MutinyWallet { /// This can be useful if you get stuck in a bad state. pub async fn reset_onchain_tracker(&mut self) -> Result<(), MutinyError> { log_trace!(self.logger, "calling reset_onchain_tracker"); - - self.node_manager.reset_onchain_tracker().await?; - // sleep for 250ms to give time for the storage to write - utils::sleep(250).await; + if let Some(node_manager) = self.node_manager.read().await.as_ref() { + node_manager.reset_onchain_tracker().await?; + // sleep for 250ms to give time for the storage to write + sleep(250).await; + } else { + return Err(MutinyError::NodeManagerRequired); + } self.stop().await?; @@ -2900,10 +3145,9 @@ impl MutinyWallet { self.start().await?; - self.node_manager - .wallet - .full_sync(FULL_SYNC_STOP_GAP) - .await?; + if let Some(node_manager) = self.node_manager.read().await.as_ref() { + node_manager.wallet.full_sync(FULL_SYNC_STOP_GAP).await?; + } log_trace!(self.logger, "finished calling reset_onchain_tracker"); Ok(()) @@ -3692,8 +3936,13 @@ impl InvoiceHandler for MutinyWallet { } async fn get_best_block(&self) -> Result { - let node = self.node_manager.get_node_by_key_or_first(None).await?; - Ok(node.channel_manager.current_best_block()) + if let Some(node_manager) = self.node_manager.read().await.as_ref() { + let node = node_manager.get_node_by_key_or_first(None).await?; + Ok(node.channel_manager.current_best_block()) + } else { + // if we don't have a node manager, just return the genesis block + Ok(BestBlock::from_network(self.network)) + } } async fn lookup_payment(&self, payment_hash: &[u8; 32]) -> Option { @@ -4061,7 +4310,7 @@ mod tests { let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher), None); - assert!(!NodeManager::has_node_manager(storage.clone())); + assert!(!NodeManager::is_wallet_present(storage.clone())); let config = MutinyWalletConfigBuilder::new(xpriv) .with_network(network) .build(); @@ -4071,7 +4320,7 @@ mod tests { .await .expect("mutiny wallet should initialize"); mw.storage.insert_mnemonic(mnemonic).unwrap(); - assert!(NodeManager::has_node_manager(storage)); + assert!(NodeManager::is_wallet_present(storage)); } #[test] @@ -4084,7 +4333,7 @@ mod tests { let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher), None); - assert!(!NodeManager::has_node_manager(storage.clone())); + assert!(!NodeManager::is_wallet_present(storage.clone())); let config = MutinyWalletConfigBuilder::new(xpriv) .with_network(network) .build(); @@ -4094,11 +4343,11 @@ mod tests { .await .expect("mutiny wallet should initialize"); - let first_seed = mw.node_manager.xprivkey; + let first_seed = mw.xprivkey; assert!(mw.stop().await.is_ok()); assert!(mw.start().await.is_ok()); - assert_eq!(first_seed, mw.node_manager.xprivkey); + assert_eq!(first_seed, mw.xprivkey); } #[test] @@ -4107,13 +4356,13 @@ mod tests { log!("{}", test_name); let network = Network::Regtest; - let xpriv = ExtendedPrivKey::new_master(network, &[0; 32]).unwrap(); + let xpriv = ExtendedPrivKey::new_master(network, &[42; 32]).unwrap(); let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher), None); - assert!(!NodeManager::has_node_manager(storage.clone())); + assert!(!NodeManager::is_wallet_present(storage.clone())); let config = MutinyWalletConfigBuilder::new(xpriv) .with_network(network) .build(); @@ -4123,20 +4372,33 @@ mod tests { .await .expect("mutiny wallet should initialize"); - // let storage persist - sleep(1000).await; + let ns = storage.get_nodes().unwrap(); + assert_eq!(ns.nodes.len(), 0); + assert_eq!(ns.version, 0); - assert_eq!(mw.node_manager.list_nodes().await.unwrap().len(), 1); + mw.create_node_manager().await.unwrap(); + let ns = storage.get_nodes().unwrap(); + assert_eq!(ns.version, 1); + assert_eq!(ns.nodes.len(), 1); - assert!(mw.node_manager.new_node().await.is_ok()); - // let storage persist - sleep(1000).await; + let lock = mw.node_manager.read().await; + let nm = lock.as_ref().unwrap(); + assert_eq!(nm.list_nodes().await.unwrap().len(), 1); - assert_eq!(mw.node_manager.list_nodes().await.unwrap().len(), 2); + nm.new_node().await.unwrap(); + let ns = storage.get_nodes().unwrap(); + assert_eq!(ns.nodes.len(), 2); + + assert_eq!(nm.list_nodes().await.unwrap().len(), 2); + drop(lock); assert!(mw.stop().await.is_ok()); + sleep(1000).await; // give it a second to stop assert!(mw.start().await.is_ok()); - assert_eq!(mw.node_manager.list_nodes().await.unwrap().len(), 2); + + let lock = mw.node_manager.read().await; + let nm = lock.as_ref().unwrap(); + assert_eq!(nm.list_nodes().await.unwrap().len(), 2); } #[test] @@ -4150,7 +4412,7 @@ mod tests { let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher), None); - assert!(!NodeManager::has_node_manager(storage.clone())); + assert!(!NodeManager::is_wallet_present(storage.clone())); let config = MutinyWalletConfigBuilder::new(xpriv) .with_network(network) .build(); @@ -4159,14 +4421,14 @@ mod tests { .build() .await .expect("mutiny wallet should initialize"); - let seed = mw.node_manager.xprivkey; + let seed = mw.xprivkey; assert!(!seed.private_key.secret_bytes().is_empty()); // create a second mw and make sure it has a different seed let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage2 = MemoryStorage::new(Some(pass), Some(cipher), None); - assert!(!NodeManager::has_node_manager(storage2.clone())); + assert!(!NodeManager::is_wallet_present(storage2.clone())); let xpriv2 = ExtendedPrivKey::new_master(network, &[0; 32]).unwrap(); let config2 = MutinyWalletConfigBuilder::new(xpriv2) .with_network(network) @@ -4176,7 +4438,7 @@ mod tests { .build() .await .expect("mutiny wallet should initialize"); - let seed2 = mw2.node_manager.xprivkey; + let seed2 = mw2.xprivkey; assert_ne!(seed, seed2); // now restore the first seed into the 2nd mutiny node @@ -4201,7 +4463,7 @@ mod tests { .build() .await .expect("mutiny wallet should initialize"); - let restored_seed = mw3.node_manager.xprivkey; + let restored_seed = mw3.xprivkey; assert_eq!(seed, restored_seed); } @@ -4217,7 +4479,7 @@ mod tests { let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher), None); - assert!(!NodeManager::has_node_manager(storage.clone())); + assert!(!NodeManager::is_wallet_present(storage.clone())); let mut config_builder = MutinyWalletConfigBuilder::new(xpriv).with_network(network); config_builder.with_safe_mode(); let config = config_builder.build(); @@ -4227,12 +4489,9 @@ mod tests { .await .expect("mutiny wallet should initialize"); mw.storage.insert_mnemonic(mnemonic).unwrap(); - assert!(NodeManager::has_node_manager(storage)); - - let bip21 = mw.create_bip21(None, vec![]).await.unwrap(); - assert!(bip21.invoice.is_none()); + assert!(NodeManager::is_wallet_present(storage)); - let new_node = mw.node_manager.new_node().await; + let new_node = mw.create_node_manager().await; assert!(new_node.is_err()); } @@ -4409,18 +4668,11 @@ mod tests { .await .expect("mutiny wallet should initialize"); - loop { - if !mw.node_manager.list_nodes().await.unwrap().is_empty() { - break; - } - sleep(100).await; - } + mw.create_node_manager().await.unwrap(); + let lock = mw.node_manager.read().await; + let node_manager = lock.as_ref().unwrap(); - let node = mw - .node_manager - .get_node_by_key_or_first(None) - .await - .unwrap(); + let node = node_manager.get_node_by_key_or_first(None).await.unwrap(); let closure: ChannelClosure = ChannelClosure { user_channel_id: None, @@ -4434,7 +4686,7 @@ mod tests { .persist_channel_closure(closure_chan_id, closure.clone()) .unwrap(); - let address = mw.node_manager.get_new_address(vec![]).unwrap(); + let address = node_manager.get_new_address(vec![]).unwrap(); let output = TxOut { value: 10_000, script_pubkey: address.script_pubkey(), @@ -4445,7 +4697,7 @@ mod tests { input: vec![], output: vec![output.clone()], }; - mw.node_manager + node_manager .wallet .insert_tx( tx1.clone(), @@ -4461,7 +4713,7 @@ mod tests { input: vec![], output: vec![output], }; - mw.node_manager + node_manager .wallet .insert_tx( tx2.clone(), @@ -4620,30 +4872,35 @@ mod tests { }, ]; + drop(lock); + assert_eq!(vec.len(), expected.len()); // make sure im not dumb assert_eq!(vec, expected); - let activity = mw.get_activity(None, None).unwrap(); + let activity = mw.get_activity(None, None).await.unwrap(); assert_eq!(activity.len(), expected.len()); - let with_limit = mw.get_activity(Some(3), None).unwrap(); + let with_limit = mw.get_activity(Some(3), None).await.unwrap(); assert_eq!(with_limit.len(), 3); - let with_offset = mw.get_activity(None, Some(3)).unwrap(); + let with_offset = mw.get_activity(None, Some(3)).await.unwrap(); assert_eq!(with_offset.len(), activity.len() - 3); - let with_both = mw.get_activity(Some(3), Some(3)).unwrap(); + let with_both = mw.get_activity(Some(3), Some(3)).await.unwrap(); assert_eq!(with_limit.len(), 3); assert_ne!(with_both, with_limit); // check we handle out of bounds errors - let with_limit_oob = mw.get_activity(Some(usize::MAX), None).unwrap(); + let with_limit_oob = mw.get_activity(Some(usize::MAX), None).await.unwrap(); assert_eq!(with_limit_oob.len(), expected.len()); - let with_offset_oob = mw.get_activity(None, Some(usize::MAX)).unwrap(); + let with_offset_oob = mw.get_activity(None, Some(usize::MAX)).await.unwrap(); assert!(with_offset_oob.is_empty()); - let with_offset_oob = mw.get_activity(None, Some(expected.len())).unwrap(); + let with_offset_oob = mw.get_activity(None, Some(expected.len())).await.unwrap(); assert!(with_offset_oob.is_empty()); - let with_both_oob = mw.get_activity(Some(usize::MAX), Some(usize::MAX)).unwrap(); + let with_both_oob = mw + .get_activity(Some(usize::MAX), Some(usize::MAX)) + .await + .unwrap(); assert!(with_both_oob.is_empty()); // update an inflight payment and make sure it isn't duplicated @@ -4663,4 +4920,118 @@ mod tests { assert!(item.is_some_and(|i| i.timestamp == Some(invoice4.last_update))); // make sure timestamp got updated assert_eq!(vec.len(), expected.len()); // make sure no duplicates } + + #[test] + async fn test_optional_node_manager() { + let test_name = "test_optional_node_manager"; + log!("{}", test_name); + + let mnemonic = generate_seed(12).unwrap(); + let network = Network::Regtest; + let xpriv = ExtendedPrivKey::new_master(network, &mnemonic.to_seed("")).unwrap(); + + let storage = MemoryStorage::new(None, None, None); + let config_builder = MutinyWalletConfigBuilder::new(xpriv).with_network(network); + let config = config_builder.build(); + let mw = MutinyWalletBuilder::new(xpriv, storage.clone()) + .with_config(config.clone()) + .build() + .await + .expect("mutiny wallet should initialize"); + + // make sure we can start with no node manager + { + let lock = mw.node_manager.read().await; + assert!(lock.is_none()); + } + + // create a node manager + let ident = mw.create_node_manager().await.unwrap(); + // verify that we have a node manager + { + let lock = mw.node_manager.read().await; + assert!(lock.is_some()); + let nodes = lock.as_ref().unwrap().list_nodes().await.unwrap(); + assert_eq!(nodes.len(), 1); + assert_eq!(ident.pubkey, *nodes.first().unwrap()); + } + + // drop the wallet, when we restart it should start with a node manager + mw.stop().await.expect("should stop"); + drop(mw); + let mw = MutinyWalletBuilder::new(xpriv, storage.clone()) + .with_config(config.clone()) + .build() + .await + .expect("mutiny wallet should initialize"); + + // we should still have a node manager + { + let lock = mw.node_manager.read().await; + assert!(lock.is_some()); + } + + // recreate the node manager, should be None because we have one + let ident2 = mw.create_node_manager_if_needed().await.unwrap(); + assert!(ident2.is_none()); + + // verify that we get the same node id + { + let lock = mw.node_manager.read().await; + let node_manager = lock.as_ref().unwrap(); + let nodes = node_manager.list_nodes().await.unwrap(); + assert_eq!(nodes.len(), 1); + assert_eq!(ident.pubkey, *nodes.first().unwrap()); + } + + // add a balance + let tx_amount = 10_000; + { + let lock = mw.node_manager.read().await; + let node_manager = lock.as_ref().unwrap(); + let address = node_manager.get_new_address(vec![]).unwrap(); + let output = TxOut { + value: tx_amount, + script_pubkey: address.script_pubkey(), + }; + let tx1 = Transaction { + version: 1, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![output], + }; + node_manager + .wallet + .insert_tx(tx1, ConfirmationTime::Unconfirmed { last_seen: 0 }, None) + .await + .unwrap(); + } + + // now when we drop the wallet and restart it should keep everything + mw.stop().await.expect("should stop"); + drop(mw); + let mw = MutinyWalletBuilder::new(xpriv, storage.clone()) + .with_config(config.clone()) + .build() + .await + .expect("mutiny wallet should initialize"); + + // verify that we get the same node id + { + let lock = mw.node_manager.read().await; + let node_manager = lock.as_ref().unwrap(); + assert!(lock.is_some()); + let nodes = node_manager.list_nodes().await.unwrap(); + assert_eq!(nodes.len(), 1); + assert_eq!(ident.pubkey, *nodes.first().unwrap()); + + // we should have the same balance + assert_eq!(node_manager.get_wallet_balance().unwrap(), tx_amount); + } + + // make sure we only ever made one node at the end of this + let node_storage = storage.get_nodes().unwrap(); + assert_eq!(node_storage.nodes.len(), 1); + assert!(node_storage.nodes.contains_key(&ident.uuid)); + } } diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 43b0e2699..b9975cc9d 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -26,7 +26,6 @@ use crate::{ }; use anyhow::anyhow; use async_lock::RwLock; -use bdk::chain::{BlockId, ConfirmationTime}; use bdk::{wallet::AddressIndex, FeeRate, LocalOutput}; use bitcoin::address::NetworkUnchecked; use bitcoin::bip32::ExtendedPrivKey; @@ -72,6 +71,12 @@ pub struct NodeStorage { pub version: u32, } +impl NodeStorage { + pub fn is_empty(&self) -> bool { + self.nodes.iter().all(|(_, n)| n.is_archived()) + } +} + // This is the NodeIndex reference that is saved to the DB #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] pub struct NodeIndex { @@ -87,8 +92,9 @@ impl NodeIndex { } } -// This is the NodeIdentity that refer to a specific node -// Used for public facing identification. +/// This is the NodeIdentity that refer to a specific node +/// Used for public facing identification. +#[derive(Debug, Clone, Eq, PartialEq)] pub struct NodeIdentity { pub uuid: String, pub pubkey: PublicKey, @@ -232,6 +238,7 @@ impl Ord for ChannelClosure { } } +#[derive(Debug, Copy, Clone, Default)] pub struct NodeBalance { pub confirmed: u64, pub unconfirmed: u64, @@ -239,6 +246,13 @@ pub struct NodeBalance { pub force_close: u64, } +impl NodeBalance { + /// All balances are zero + pub fn is_zero(&self) -> bool { + self.confirmed == 0 && self.unconfirmed == 0 && self.lightning == 0 && self.force_close == 0 + } +} + pub struct NodeManagerBuilder { xprivkey: ExtendedPrivKey, storage: S, @@ -447,6 +461,11 @@ impl NodeManagerBuilder { let storage = self.storage.clone(); let logger_clone = logger.clone(); spawn(async move { + // skip if there are no nodes + if updated_nodes.is_empty() { + return; + } + let start = Instant::now(); if let Err(e) = storage .insert_nodes(&NodeStorage { @@ -533,7 +552,7 @@ pub struct NodeManager { impl NodeManager { /// Returns if there is a saved wallet in storage. /// This is checked by seeing if a mnemonic seed exists in storage. - pub fn has_node_manager(storage: S) -> bool { + pub fn is_wallet_present(storage: S) -> bool { storage.get_mnemonic().is_ok_and(|x| x.is_some()) } @@ -582,56 +601,36 @@ impl NodeManager { /// Creates a background process that will sync the wallet with the blockchain. /// This will also update the fee estimates every 10 minutes. - pub fn start_sync(nm: Arc>) { - log_trace!(nm.logger, "calling start_sync"); + pub fn start_sync( + nm: Arc>>>, + network: Network, + stop: Arc, + logger: Arc, + ) { + log_trace!(logger, "calling start_sync"); // sync every second on regtest, this makes testing easier - let sync_interval_secs = match nm.network { + let sync_interval_secs = match network { Network::Bitcoin | Network::Testnet | Network::Signet => 60, Network::Regtest => 1, net => unreachable!("Unknown network: {net}"), }; + utils::spawn(async move { let mut synced = false; loop { // If we are stopped, don't sync - if nm.stop.load(Ordering::Relaxed) { + if stop.load(Ordering::Relaxed) { return; } - if !synced { - if let Err(e) = nm.sync_rgs().await { - log_error!(nm.logger, "Failed to sync RGS: {e}"); - } else { - log_info!(nm.logger, "RGS Synced!"); - } - - if let Err(e) = nm.sync_scorer().await { - log_error!(nm.logger, "Failed to sync scorer: {e}"); - } else { - log_info!(nm.logger, "Scorer Synced!"); - } - } - - // we don't need to re-sync fees every time - // just do it every 10 minutes - if let Err(e) = nm.fee_estimator.update_fee_estimates_if_necessary().await { - log_error!(nm.logger, "Failed to update fee estimates: {e}"); - } else { - log_info!(nm.logger, "Updated fee estimates!"); - } - - if let Err(e) = nm.sync().await { - log_error!(nm.logger, "Failed to sync: {e}"); - } else if !synced { - // if this is the first sync, set the done_first_sync flag - let _ = nm.storage.set_done_first_sync(); - synced = true; + if let Some(nm) = nm.read().await.as_ref() { + nm.do_sync_round(&mut synced).await; } // wait for next sync round, checking graceful shutdown check each second. for _ in 0..sync_interval_secs { - if nm.stop.load(Ordering::Relaxed) { + if stop.load(Ordering::Relaxed) { return; } sleep(1_000).await; @@ -640,6 +639,38 @@ impl NodeManager { }); } + pub(crate) async fn do_sync_round(&self, synced: &mut bool) { + if !*synced { + if let Err(e) = self.sync_rgs().await { + log_error!(self.logger, "Failed to sync RGS: {e}"); + } else { + log_info!(self.logger, "RGS Synced!"); + } + + if let Err(e) = self.sync_scorer().await { + log_error!(self.logger, "Failed to sync scorer: {e}"); + } else { + log_info!(self.logger, "Scorer Synced!"); + } + } + + // we don't need to re-sync fees every time + // just do it every 10 minutes + if let Err(e) = self.fee_estimator.update_fee_estimates_if_necessary().await { + log_error!(self.logger, "Failed to update fee estimates: {e}"); + } else { + log_info!(self.logger, "Updated fee estimates!"); + } + + if let Err(e) = self.sync().await { + log_error!(self.logger, "Failed to sync: {e}"); + } else if !*synced { + // if this is the first sync, set the done_first_sync flag + let _ = self.storage.set_done_first_sync(); + *synced = true; + } + } + /// Broadcast a transaction to the network. /// The transaction is broadcast through the configured esplora server. pub async fn broadcast_transaction(&self, tx: Transaction) -> Result<(), MutinyError> { @@ -662,7 +693,7 @@ impl NodeManager { if let Ok(mut wallet) = self.wallet.wallet.try_write() { let address = wallet.try_get_address(AddressIndex::LastUnused)?.address; - self.set_address_labels(address.clone(), labels)?; + self.storage.set_address_labels(address.clone(), labels)?; log_trace!(self.logger, "finished calling get_new_address"); return Ok(address); @@ -891,86 +922,6 @@ impl NodeManager { res } - /// Checks if the given address has any transactions. - /// If it does, it returns the details of the first transaction. - /// - /// This should be used to check if a payment has been made to an address. - pub async fn check_address( - &self, - address: Address, - ) -> Result, MutinyError> { - log_trace!(self.logger, "calling check_address"); - - let address = address.require_network(self.network)?; - - let script = address.payload.script_pubkey(); - let txs = self.esplora.scripthash_txs(&script, None).await?; - - let details_opt = txs.first().map(|tx| { - let received: u64 = tx - .vout - .iter() - .filter(|v| v.scriptpubkey == script) - .map(|v| v.value) - .sum(); - - let confirmation_time = tx - .confirmation_time() - .map(|c| ConfirmationTime::Confirmed { - height: c.height, - time: c.timestamp, - }) - .unwrap_or(ConfirmationTime::Unconfirmed { - last_seen: utils::now().as_secs(), - }); - - let address_labels = self.get_address_labels().unwrap_or_default(); - let labels = address_labels - .get(&address.to_string()) - .cloned() - .unwrap_or_default(); - - let details = TransactionDetails { - transaction: Some(tx.to_tx()), - txid: Some(tx.txid), - internal_id: tx.txid, - received, - sent: 0, - fee: None, - confirmation_time, - labels, - }; - - let block_id = match tx.status.block_hash { - Some(hash) => { - let height = tx - .status - .block_height - .expect("block height must be present"); - Some(BlockId { hash, height }) - } - None => None, - }; - - (details, block_id) - }); - - // if we found a tx we should try to import it into the wallet - if let Some((details, block_id)) = details_opt.clone() { - let wallet = self.wallet.clone(); - utils::spawn(async move { - let tx = details.transaction.expect("tx must be present"); - wallet - .insert_tx(tx, details.confirmation_time, block_id) - .await - .expect("failed to insert tx"); - }); - } - - log_trace!(self.logger, "finished calling check_address"); - Ok(details_opt.map(|(d, _)| d)) - } - /// Adds labels to the TransactionDetails based on the address labels. /// This will panic if the TransactionDetails does not have a transaction. /// Make sure you flag `include_raw` when calling `list_transactions` to @@ -1006,7 +957,7 @@ impl NodeManager { let mut txs = self.wallet.list_transactions(true)?; txs.sort(); - let address_labels = self.get_address_labels()?; + let address_labels = self.storage.get_address_labels()?; let txs = txs .into_iter() .map(|tx| self.add_onchain_labels(&address_labels, tx)) @@ -1022,7 +973,7 @@ impl NodeManager { let res = match self.wallet.get_transaction(txid)? { Some(tx) => { - let address_labels = self.get_address_labels()?; + let address_labels = self.storage.get_address_labels()?; let tx_details = self.add_onchain_labels(&address_labels, tx); Ok(Some(tx_details)) } @@ -1295,7 +1246,6 @@ impl NodeManager { /// Archives a node so it will not be started up next time the node manager is created. /// /// If the node has any active channels it will fail to archive - #[allow(dead_code)] pub(crate) async fn archive_node(&self, pubkey: PublicKey) -> Result<(), MutinyError> { if let Some(node) = self.nodes.read().await.get(&pubkey) { // disallow archiving nodes with active channels or @@ -1315,7 +1265,6 @@ impl NodeManager { /// Archives a node so it will not be started up next time the node manager is created. /// /// If the node has any active channels it will fail to archive - #[allow(dead_code)] pub(crate) async fn archive_node_by_uuid(&self, node_uuid: String) -> Result<(), MutinyError> { let mut node_storage = self.node_storage.write().await; @@ -1333,6 +1282,104 @@ impl NodeManager { } } + /// Unarchives the first node in the node manager if we have one. + pub(crate) async fn unarchive_first_node(&self) -> Result { + // get locks + let mut node_storage = self.node_storage.write().await; + let mut nodes_map = self.nodes.write().await; + + let node = match node_storage.nodes.iter_mut().next() { + None => return Err(MutinyError::NotFound), + Some((uuid, node)) => { + // if isn't archived, just return the node + if !node.is_archived() { + let (_, node) = nodes_map + .iter() + .find(|(_, n)| &n.uuid == uuid) + .ok_or(MutinyError::NotFound)?; + return Ok(NodeIdentity { + uuid: node.uuid.clone(), + pubkey: node.pubkey, + }); + } + + node.archived = None; + + let mut node_builder = NodeBuilder::new(self.xprivkey, self.storage.clone()) + .with_uuid(uuid.clone()) + .with_node_index(node.clone()) + .with_gossip_sync(self.gossip_sync.clone()) + .with_scorer(self.scorer.clone()) + .with_chain(self.chain.clone()) + .with_fee_estimator(self.fee_estimator.clone()) + .with_wallet(self.wallet.clone()) + .with_esplora(self.esplora.clone()) + .with_network(self.network); + node_builder.with_logger(self.logger.clone()); + + #[cfg(target_arch = "wasm32")] + node_builder.with_websocket_proxy_addr(self.websocket_proxy_addr.clone()); + + if let Some(l) = self.lsp_config.clone() { + node_builder.with_lsp_config(l); + } + if self.do_not_connect_peers { + node_builder.do_not_connect_peers(); + } + + node_builder.build().await? + } + }; + + let pubkey = node + .keys_manager + .get_node_id(Recipient::Node) + .expect("Failed to get node id"); + + let uuid = node.uuid.clone(); + nodes_map.insert(pubkey, Arc::new(node)); + + node_storage.version += 1; + self.storage.insert_nodes(&node_storage).await?; + + Ok(NodeIdentity { uuid, pubkey }) + } + + /// Creates the first node for the NodeManager. This will handle if we need to create a new node + /// or unarchive the first node. + pub async fn create_first_node(&self) -> Result { + let needs_new_node = { + let node_storage = self.node_storage.read().await; + node_storage.nodes.is_empty() + }; + + // if we've never created a node, we need to create one + // otherwise we need to unarchive the first node + if needs_new_node { + self.new_node().await + } else { + self.unarchive_first_node().await + } + } + + /// Checks if the node manager is unused. This is used to determine if we are safe to remove the node manager + pub async fn is_unused(&self) -> Result { + // if we have any balances, we are not unused + if !self.get_balance().await?.is_zero() { + return Ok(false); + } + + // if we have any monitors, we are not unused + let nodes = self.nodes.read().await; + for (_, n) in nodes.iter() { + if !n.chain_monitor.get_claimable_balances(&[]).is_empty() { + return Ok(false); + } + } + + Ok(true) + } + /// Lists the pubkeys of the lightning node in the manager. pub async fn list_nodes(&self) -> Result, MutinyError> { log_trace!(self.logger, "calling list_nodes"); @@ -2045,9 +2092,12 @@ pub(crate) async fn create_new_node_from_node_manager( let next_node_uuid = new_node.uuid.clone(); existing_nodes.version += 1; - existing_nodes + let old = existing_nodes .nodes .insert(next_node_uuid.clone(), next_node); + + debug_assert!(old.is_none(), "Node index should not exist in storage"); + node_manager.storage.insert_nodes(&existing_nodes).await?; node_mutex.nodes = existing_nodes.nodes.clone(); @@ -2136,7 +2186,7 @@ mod tests { let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher), None); - assert!(!NodeManager::has_node_manager(storage.clone())); + assert!(!NodeManager::is_wallet_present(storage.clone())); let c = MutinyWalletConfigBuilder::new(xpriv) .with_network(network) .build(); @@ -2146,7 +2196,7 @@ mod tests { .await .expect("node manager should initialize"); storage.insert_mnemonic(seed).unwrap(); - assert!(NodeManager::has_node_manager(storage)); + assert!(NodeManager::is_wallet_present(storage)); } #[test] diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index 3e7b70ff8..e760da21f 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -174,6 +174,9 @@ pub enum MutinyJsError { /// Failed to connect to a federation. #[error("Failed to connect to a federation.")] FederationConnectionFailed, + /// A node manager has not been created yet. + #[error("A node manager has not been created yet.")] + NodeManagerRequired, /// Unknown error. #[error("Unknown Error")] UnknownError, @@ -228,6 +231,7 @@ impl From for MutinyJsError { MutinyError::TokenAlreadySpent => MutinyJsError::TokenAlreadySpent, MutinyError::FederationRequired => MutinyJsError::FederationRequired, MutinyError::FederationConnectionFailed => MutinyJsError::FederationConnectionFailed, + MutinyError::NodeManagerRequired => MutinyJsError::NodeManagerRequired, MutinyError::Other(_) => MutinyJsError::UnknownError, MutinyError::SubscriptionClientNotConfigured => { MutinyJsError::SubscriptionClientNotConfigured diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 47197caef..fe2a1f690 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -340,7 +340,7 @@ impl MutinyWallet { /// Returns if there is a saved wallet in storage. /// This is checked by seeing if a mnemonic seed exists in storage. #[wasm_bindgen] - pub async fn has_node_manager() -> Result { + pub async fn is_wallet_present() -> Result { Ok(IndexedDbStorage::has_mnemonic().await?) } @@ -419,7 +419,7 @@ impl MutinyWallet { /// Returns after node has been stopped. #[wasm_bindgen] pub async fn stop(&self) -> Result<(), MutinyJsError> { - Ok(self.inner.node_manager.stop().await?) + Ok(self.inner.stop().await?) } /// Returns the mnemonic seed phrase for the wallet. @@ -565,12 +565,14 @@ impl MutinyWallet { // I know walia parses `pj=` and `pjos=` but payjoin::Uri parses the whole bip21 uri let pj_uri = payjoin::Uri::try_from(payjoin_uri.as_str()) .map_err(|_| MutinyJsError::InvalidArgumentsError)?; - Ok(self - .inner - .node_manager - .send_payjoin(pj_uri, amount, labels, fee_rate) - .await? - .to_string()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm + .send_payjoin(pj_uri, amount, labels, fee_rate) + .await? + .to_string()) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Sweeps all the funds from the wallet to the given address. @@ -620,27 +622,29 @@ impl MutinyWallet { /// Estimates the onchain fee for a opening a lightning channel. /// The amount is in satoshis and the fee rate is in sat/vbyte. - pub fn estimate_channel_open_fee( + pub async fn estimate_channel_open_fee( &self, amount: u64, fee_rate: Option, ) -> Result { - Ok(self - .inner - .node_manager - .estimate_channel_open_fee(amount, fee_rate)?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.estimate_channel_open_fee(amount, fee_rate)?) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Estimates the onchain fee for sweeping our on-chain balance to open a lightning channel. /// The fee rate is in sat/vbyte. - pub fn estimate_sweep_channel_open_fee( + pub async fn estimate_sweep_channel_open_fee( &self, fee_rate: Option, ) -> Result { - Ok(self - .inner - .node_manager - .estimate_sweep_channel_open_fee(fee_rate)?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.estimate_sweep_channel_open_fee(fee_rate)?) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Estimates the lightning fee for a transaction. Amount is either from the invoice @@ -667,9 +671,12 @@ impl MutinyWallet { /// the new given fee rate in sats/vbyte pub async fn bump_fee(&self, txid: String, fee_rate: f32) -> Result { let txid = Txid::from_str(&txid)?; - let result = self.inner.node_manager.bump_fee(txid, fee_rate).await?; - - Ok(result.to_string()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + let result = nm.bump_fee(txid, fee_rate).await?; + Ok(result.to_string()) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Checks if the given address has any transactions. @@ -682,19 +689,21 @@ impl MutinyWallet { address: String, ) -> Result */, MutinyJsError> { let address = Address::from_str(&address)?; - Ok(JsValue::from_serde( - &self.inner.node_manager.check_address(address).await?, - )?) + + let result = self.inner.check_address(address).await?; + Ok(JsValue::from_serde(&result)?) } /// Gets the details of a specific on-chain transaction. #[wasm_bindgen] - pub fn get_transaction( + pub async fn get_transaction( &self, txid: String, ) -> Result */, MutinyJsError> { let txid = Txid::from_str(&txid)?; - Ok(JsValue::from_serde(&self.inner.get_transaction(txid)?)?) + Ok(JsValue::from_serde( + &self.inner.get_transaction(txid).await?, + )?) } /// Gets the current balance of the wallet. @@ -708,43 +717,85 @@ impl MutinyWallet { /// Lists all the UTXOs in the wallet. #[wasm_bindgen] - pub fn list_utxos(&self) -> Result { - Ok(JsValue::from_serde(&self.inner.node_manager.list_utxos()?)?) + pub async fn list_utxos(&self) -> Result { + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + let result = nm.list_utxos()?; + Ok(JsValue::from_serde(&result)?) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } - /// Gets a fee estimate for an low priority transaction. + /// Gets a fee estimate for a low priority transaction. /// Value is in sat/vbyte. #[wasm_bindgen] - pub fn estimate_fee_low(&self) -> u32 { - self.inner.node_manager.estimate_fee_low() + pub async fn estimate_fee_low(&self) -> Result { + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.estimate_fee_low()) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Gets a fee estimate for an average priority transaction. /// Value is in sat/vbyte. #[wasm_bindgen] - pub fn estimate_fee_normal(&self) -> u32 { - self.inner.node_manager.estimate_fee_normal() + pub async fn estimate_fee_normal(&self) -> Result { + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.estimate_fee_normal()) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } - /// Gets a fee estimate for an high priority transaction. + /// Gets a fee estimate for a high priority transaction. /// Value is in sat/vbyte. #[wasm_bindgen] - pub fn estimate_fee_high(&self) -> u32 { - self.inner.node_manager.estimate_fee_high() + pub async fn estimate_fee_high(&self) -> Result { + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.estimate_fee_high()) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Creates a new lightning node and adds it to the manager. #[wasm_bindgen] pub async fn new_node(&self) -> Result { - Ok(self.inner.node_manager.new_node().await?.into()) + let lock = self.inner.node_manager.read().await; + if let Some(nm) = lock.as_ref() { + Ok(nm.new_node().await?.into()) + } else { + drop(lock); // drop the lock so we can get a write lock to create the node manager + Ok(self.inner.create_node_manager().await?.into()) + } + } + + /// Creates a node manager if we don't have one already. + #[wasm_bindgen] + pub async fn create_node_manager_if_needed(&self) -> Result<(), MutinyJsError> { + self.inner.create_node_manager_if_needed().await?; + + Ok(()) + } + + /// Removes the node manager from the wallet. If the node manager has any balances, + /// this function will fail. + #[wasm_bindgen] + pub async fn remove_node_manager(&self) -> Result<(), MutinyJsError> { + self.inner.remove_node_manager().await?; + + Ok(()) } /// Lists the pubkeys of the lightning node in the manager. #[wasm_bindgen] pub async fn list_nodes(&self) -> Result */, MutinyJsError> { - Ok(JsValue::from_serde( - &self.inner.node_manager.list_nodes().await?, - )?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(JsValue::from_serde(&nm.list_nodes().await?)?) + } else { + Ok(JsValue::from_serde::>(&vec![])?) + } } /// Changes all the node's LSPs to the given config. If any of the nodes have an active channel with the @@ -758,9 +809,11 @@ impl MutinyWallet { lsp_token: Option, ) -> Result<(), MutinyJsError> { let lsp_config = create_lsp_config(lsp_url, lsp_connection_string, lsp_token)?; - - self.inner.node_manager.change_lsp(lsp_config).await?; - Ok(()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.change_lsp(lsp_config).await?) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Attempts to connect to a peer from the selected node. @@ -770,18 +823,22 @@ impl MutinyWallet { connection_string: String, label: Option, ) -> Result<(), MutinyJsError> { - Ok(self - .inner - .node_manager - .connect_to_peer(None, &connection_string, label) - .await?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.connect_to_peer(None, &connection_string, label).await?) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Disconnects from a peer from the selected node. #[wasm_bindgen] pub async fn disconnect_peer(&self, peer: String) -> Result<(), MutinyJsError> { let peer = PublicKey::from_str(&peer)?; - Ok(self.inner.node_manager.disconnect_peer(None, peer).await?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.disconnect_peer(None, peer).await?) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Deletes a peer from the selected node. @@ -790,16 +847,27 @@ impl MutinyWallet { #[wasm_bindgen] pub async fn delete_peer(&self, peer: String) -> Result<(), MutinyJsError> { let peer = NodeId::from_str(&peer).map_err(|_| MutinyJsError::InvalidArgumentsError)?; - Ok(self.inner.node_manager.delete_peer(None, &peer).await?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.delete_peer(None, &peer).await?) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Sets the label of a peer from the selected node. #[wasm_bindgen] - pub fn label_peer(&self, node_id: String, label: Option) -> Result<(), MutinyJsError> { + pub async fn label_peer( + &self, + node_id: String, + label: Option, + ) -> Result<(), MutinyJsError> { let node_id = NodeId::from_str(&node_id).map_err(|_| MutinyJsError::InvalidArgumentsError)?; - self.inner.node_manager.label_peer(&node_id, label)?; - Ok(()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.label_peer(&node_id, label)?) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Creates a lightning invoice. The amount should be in satoshis. @@ -846,12 +914,14 @@ impl MutinyWallet { labels: Vec, ) -> Result { let to_node = PublicKey::from_str(&to_node)?; - Ok(self - .inner - .node_manager - .keysend(None, to_node, amt_sats, message, labels) - .await? - .into()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm + .keysend(None, to_node, amt_sats, message, labels) + .await? + .into()) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Decodes a lightning invoice into useful information. @@ -977,12 +1047,14 @@ impl MutinyWallet { user_channel_id: String, ) -> Result { let user_channel_id: [u8; 16] = FromHex::from_hex(&user_channel_id)?; - Ok(self - .inner - .node_manager - .get_channel_closure(u128::from_be_bytes(user_channel_id)) - .await? - .into()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm + .get_channel_closure(u128::from_be_bytes(user_channel_id)) + .await? + .into()) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Gets all channel closures from the node manager. @@ -992,9 +1064,14 @@ impl MutinyWallet { pub async fn list_channel_closures( &self, ) -> Result */, MutinyJsError> { - let mut channel_closures = self.inner.node_manager.list_channel_closures().await?; - channel_closures.sort(); - Ok(JsValue::from_serde(&channel_closures)?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + let mut channel_closures = nm.list_channel_closures().await?; + channel_closures.sort(); + Ok(JsValue::from_serde(&channel_closures)?) + } else { + // just return empty list if no node manager + Ok(JsValue::from_serde::>(&vec![])?) + } } /// Opens a channel from our selected node to the given pubkey. @@ -1016,12 +1093,14 @@ impl MutinyWallet { _ => None, }; - Ok(self - .inner - .node_manager - .open_channel(None, to_pubkey, amount, fee_rate, None) - .await? - .into()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm + .open_channel(None, to_pubkey, amount, fee_rate, None) + .await? + .into()) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Opens a channel from our selected node to the given pubkey. @@ -1039,12 +1118,11 @@ impl MutinyWallet { _ => None, }; - Ok(self - .inner - .node_manager - .sweep_all_to_channel(to_pubkey) - .await? - .into()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.sweep_all_to_channel(to_pubkey).await?.into()) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Sweep the federation balance into a lightning channel @@ -1115,27 +1193,32 @@ impl MutinyWallet { ) -> Result<(), MutinyJsError> { let outpoint: OutPoint = OutPoint::from_str(&outpoint).map_err(|_| MutinyJsError::InvalidArgumentsError)?; - Ok(self - .inner - .node_manager - .close_channel(&outpoint, None, force, abandon) - .await?) + + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.close_channel(&outpoint, None, force, abandon).await?) + } else { + Err(MutinyJsError::NodeManagerRequired) + } } /// Lists all the channels for all the nodes in the node manager. #[wasm_bindgen] pub async fn list_channels(&self) -> Result */, MutinyJsError> { - Ok(JsValue::from_serde( - &self.inner.node_manager.list_channels().await?, - )?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(JsValue::from_serde(&nm.list_channels().await?)?) + } else { + Ok(JsValue::from_serde::>(&vec![])?) + } } /// Lists all the peers for all the nodes in the node manager. #[wasm_bindgen] pub async fn list_peers(&self) -> Result */, MutinyJsError> { - Ok(JsValue::from_serde( - &self.inner.node_manager.list_peers().await?, - )?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(JsValue::from_serde(&nm.list_peers().await?)?) + } else { + Ok(JsValue::from_serde::>(&vec![])?) + } } /// Returns all the on-chain and lightning activity from the wallet. @@ -1146,11 +1229,11 @@ impl MutinyWallet { offset: Option, ) -> Result */, MutinyJsError> { // get activity from the node manager - let activity = self.inner.get_activity(limit, offset)?; + let activity = self.inner.get_activity(limit, offset).await?; let mut activity: Vec = activity.into_iter().map(|a| a.into()).collect(); // add contacts to the activity - let contacts = self.inner.node_manager.get_contacts()?; + let contacts = self.inner.get_contacts()?; let follows = self.inner.nostr.get_follow_list()?; for a in activity.iter_mut() { // find labels that have a contact and add them to the item @@ -1183,7 +1266,7 @@ impl MutinyWallet { let mut activity: Vec = activity.into_iter().map(|a| a.into()).collect(); // add contact to the activity item it has one, otherwise return the activity list - let contact = match self.inner.node_manager.get_contact(&label)? { + let contact = match self.inner.get_contact(&label)? { Some(contact) => contact, None => return Ok(JsValue::from_serde(&activity)?), }; @@ -1310,9 +1393,7 @@ impl MutinyWallet { pub fn get_address_labels( &self, ) -> Result> */, MutinyJsError> { - Ok(JsValue::from_serde( - &self.inner.node_manager.get_address_labels()?, - )?) + Ok(JsValue::from_serde(&self.inner.get_address_labels()?)?) } /// Set the labels for an address, replacing any existing labels @@ -1324,18 +1405,13 @@ impl MutinyWallet { labels: Vec, ) -> Result<(), MutinyJsError> { let address = Address::from_str(&address)?.assume_checked(); - Ok(self - .inner - .node_manager - .set_address_labels(address, labels)?) + Ok(self.inner.set_address_labels(address, labels)?) } pub fn get_invoice_labels( &self, ) -> Result> */, MutinyJsError> { - Ok(JsValue::from_serde( - &self.inner.node_manager.get_invoice_labels()?, - )?) + Ok(JsValue::from_serde(&self.inner.get_invoice_labels()?)?) } /// Set the labels for an invoice, replacing any existing labels @@ -1347,10 +1423,7 @@ impl MutinyWallet { labels: Vec, ) -> Result<(), MutinyJsError> { let invoice = Bolt11Invoice::from_str(&invoice)?; - Ok(self - .inner - .node_manager - .set_invoice_labels(invoice, labels)?) + Ok(self.inner.set_invoice_labels(invoice, labels)?) } pub async fn get_contacts(&self) -> Result*/, MutinyJsError> { @@ -1358,7 +1431,6 @@ impl MutinyWallet { Ok(JsValue::from_serde( &self .inner - .node_manager .get_contacts()? .into_iter() .map(|(id, c)| { @@ -1378,7 +1450,6 @@ impl MutinyWallet { let follows = self.inner.nostr.get_follow_list()?; let mut contacts: Vec = self .inner - .node_manager .get_contacts()? .into_iter() .map(|(id, c)| { @@ -1401,7 +1472,6 @@ impl MutinyWallet { let follows = self.inner.nostr.get_follow_list()?; let mut contacts: Vec = self .inner - .node_manager .get_contacts()? .into_iter() .flat_map(|(id, c)| { @@ -1424,7 +1494,7 @@ impl MutinyWallet { } pub fn get_tag_item(&self, label: String) -> Result, MutinyJsError> { - match self.inner.node_manager.get_contact(&label)? { + match self.inner.get_contact(&label)? { Some(contact) => { let follows = self.inner.nostr.get_follow_list()?; let is_followed = contact @@ -1459,10 +1529,7 @@ impl MutinyWallet { last_used: now().as_secs(), }; - Ok(self - .inner - .node_manager - .create_contact_from_label(label, contact)?) + Ok(self.inner.create_contact_from_label(label, contact)?) } pub fn create_new_contact( @@ -1483,11 +1550,11 @@ impl MutinyWallet { image_url, last_used: now().as_secs(), }; - Ok(self.inner.node_manager.create_new_contact(contact)?) + Ok(self.inner.create_new_contact(contact)?) } pub fn delete_contact(&self, id: String) -> Result<(), MutinyJsError> { - Ok(self.inner.node_manager.delete_contact(id)?) + Ok(self.inner.delete_contact(id)?) } pub fn edit_contact( @@ -1510,7 +1577,7 @@ impl MutinyWallet { last_used: now().as_secs(), }; - Ok(self.inner.node_manager.edit_contact(id, contact)?) + Ok(self.inner.edit_contact(id, contact)?) } pub async fn get_contact_for_npub( @@ -1518,7 +1585,7 @@ impl MutinyWallet { npub: String, ) -> Result, MutinyJsError> { let npub = parse_npub(&npub)?; - let contact = self.inner.node_manager.get_contact_for_npub(npub)?; + let contact = self.inner.get_contact_for_npub(npub)?; match contact { Some((id, c)) => { @@ -1537,7 +1604,6 @@ impl MutinyWallet { pub fn get_tag_items(&self) -> Result, MutinyJsError> { let mut tags: Vec = self .inner - .node_manager .get_tag_items()? .into_iter() .map(|t| t.into()) @@ -2044,9 +2110,12 @@ impl MutinyWallet { /// Resets the scorer and network graph. This can be useful if you get stuck in a bad state. #[wasm_bindgen] pub async fn reset_router(&self) -> Result<(), MutinyJsError> { - self.inner.node_manager.reset_router().await?; - // Sleep to wait for indexed db to finish writing - sleep(500).await; + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + nm.reset_router().await?; + // Sleep to wait for indexed db to finish writing + sleep(500).await; + } + Ok(()) } @@ -2201,7 +2270,7 @@ mod tests { log!("creating mutiny wallet!"); let password = Some("password".to_string()); - assert!(!MutinyWallet::has_node_manager().await.unwrap()); + assert!(!MutinyWallet::is_wallet_present().await.unwrap()); MutinyWallet::new( password.clone(), None, @@ -2229,7 +2298,7 @@ mod tests { .await .expect("mutiny wallet should initialize"); sleep(1_000).await; - assert!(MutinyWallet::has_node_manager().await.unwrap()); + assert!(MutinyWallet::is_wallet_present().await.unwrap()); IndexedDbStorage::clear() .await @@ -2266,7 +2335,7 @@ mod tests { .await .expect("mutiny wallet should initialize"); sleep(1_000).await; - assert!(MutinyWallet::has_node_manager().await.unwrap()); + assert!(MutinyWallet::is_wallet_present().await.unwrap()); uninit().await; let seed = mutiny_core::generate_seed(12).unwrap(); @@ -2313,7 +2382,7 @@ mod tests { log!("trying to create 2 mutiny wallets!"); let password = Some("password".to_string()); - assert!(!MutinyWallet::has_node_manager().await.unwrap()); + assert!(!MutinyWallet::is_wallet_present().await.unwrap()); MutinyWallet::new( password.clone(), None, @@ -2341,7 +2410,7 @@ mod tests { .await .expect("mutiny wallet should initialize"); sleep(1_000).await; - assert!(MutinyWallet::has_node_manager().await.unwrap()); + assert!(MutinyWallet::is_wallet_present().await.unwrap()); // try to create a second let result = MutinyWallet::new( @@ -2423,7 +2492,7 @@ mod tests { .unwrap(); log!("checking nm"); - assert!(MutinyWallet::has_node_manager().await.unwrap()); + assert!(MutinyWallet::is_wallet_present().await.unwrap()); log!("checking seed"); assert_eq!(seed.to_string(), nm.show_seed()); @@ -2472,7 +2541,7 @@ mod tests { .unwrap(); log!("checking nm"); - assert!(MutinyWallet::has_node_manager().await.unwrap()); + assert!(MutinyWallet::is_wallet_present().await.unwrap()); log!("checking seed"); assert_eq!(seed.to_string(), nm.show_seed()); nm.stop().await.unwrap(); @@ -2551,13 +2620,12 @@ mod tests { assert_ne!("", node_identity.uuid()); assert_ne!("", node_identity.pubkey()); - let node_identity = nm + let node_identity2 = nm .new_node() .await .expect("mutiny wallet should initialize"); - assert_ne!("", node_identity.uuid()); - assert_ne!("", node_identity.pubkey()); + assert_ne!(node_identity, node_identity2); IndexedDbStorage::clear() .await diff --git a/mutiny-wasm/src/models.rs b/mutiny-wasm/src/models.rs index 7978c2e04..98aef67ae 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -559,7 +559,7 @@ impl From for LnUrlParams { // This is the NodeIdentity that refer to a specific node // Used for public facing identification. -#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] #[wasm_bindgen] pub struct NodeIdentity { uuid: String,