From 23692b2461e354c9511e4079784ef875c447d12d Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 15 Apr 2024 19:22:21 -0500 Subject: [PATCH] Make NodeManager optional --- mutiny-core/src/error.rs | 4 + mutiny-core/src/labels.rs | 4 +- mutiny-core/src/lib.rs | 443 ++++++++++++++++++++++----------- mutiny-core/src/nodemanager.rs | 120 +++++---- mutiny-wasm/src/error.rs | 4 + mutiny-wasm/src/lib.rs | 425 ++++++++++++++++++------------- mutiny-wasm/src/models.rs | 2 +- 7 files changed, 631 insertions(+), 371 deletions(-) diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index 2ccee84d8..fdf590749 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -178,6 +178,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(#[from] anyhow::Error), } @@ -262,6 +265,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 626f47307..afa3d0f77 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -94,7 +94,7 @@ use bip39::Mnemonic; use bitcoin::bip32::ExtendedPrivKey; use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, ThirtyTwoByteHash}; -use bitcoin::{hashes::sha256, Network, Txid}; +use bitcoin::{hashes::sha256, Address, Network, Txid}; use fedimint_core::{api::InviteCode, config::FederationId}; use futures::{pin_mut, select, FutureExt}; use futures_util::join; @@ -128,6 +128,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::*}; @@ -838,20 +839,25 @@ impl MutinyWalletBuilder { let start = Instant::now(); - let mut nm_builder = NodeManagerBuilder::new(self.xprivkey, self.storage.clone()) - .with_config(config.clone()); - nm_builder.with_stop(stop.clone()); - nm_builder.with_logger(logger.clone()); - let node_manager = Arc::new(nm_builder.build().await?); + 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()); + let node_manager = Arc::new(RwLock::new(Some(nm_builder.build().await?))); - log_trace!( - logger, - "NodeManager started, took: {}ms", - start.elapsed().as_millis() - ); + // start sync + NodeManager::start_sync(node_manager.clone(), stop.clone()); - // start syncing node manager - NodeManager::start_sync(node_manager.clone()); + log_trace!( + logger, + "NodeManager started, took: {}ms", + start.elapsed().as_millis() + ); + + node_manager + }; let primal_client = PrimalClient::new( config @@ -984,18 +990,27 @@ impl MutinyWalletBuilder { }; // populate the 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.txid), - }) - .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.txid), + }) + .collect::>() + }) + .unwrap_or_default() + }; // add the channel closures to the activity index let closures = self @@ -1077,17 +1092,6 @@ impl MutinyWalletBuilder { return Ok(mw); } - // if we don't have any nodes, create one - if mw.node_manager.list_nodes().await?.is_empty() { - 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}"); - } - }) - }; - // start the nostr background process mw.start_nostr().await; @@ -1124,7 +1128,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>>>>, @@ -1144,18 +1148,26 @@ 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> { 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_stop(self.stop.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.stop.clone()); Ok(()) } @@ -1333,6 +1345,85 @@ impl MutinyWallet { }); } + /// 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 { + 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 lock.is_some() { + return Err(MutinyError::AlreadyRunning); + } + + let mut nm_builder = NodeManagerBuilder::new(self.xprivkey, self.storage.clone()) + .with_config(self.config.clone()); + nm_builder.with_stop(self.stop.clone()); + nm_builder.with_logger(self.logger.clone()); + let node_manager = nm_builder.build().await?; + + // create first node + let node_identity = node_manager.new_node().await?; + + // replace the node manager + *lock = Some(node_manager); + + // start sync + NodeManager::start_sync(self.node_manager.clone(), self.stop.clone()); + + 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(_) => Ok(()), + Err(MutinyError::AlreadyRunning) => Ok(()), // 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> { + 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 !node_manager.get_balance().await?.is_zero() { + return Err(MutinyError::InvalidArgumentsError); + } + + // archive all nodes + 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; + + 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. @@ -1413,33 +1504,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. - 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?; + 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)) } @@ -1539,8 +1632,9 @@ impl MutinyWallet { ) }; - let Ok(address) = self.node_manager.get_new_address(labels.clone()) else { - return Err(MutinyError::WalletOperationFailed); + let address = match self.node_manager.read().await.as_ref() { + Some(node_manager) => node_manager.get_new_address(labels.clone())?, + None => Address::p2sh(bitcoin::Script::empty(), self.network).unwrap(), // fixme temp replacement for fedimint }; Ok(MutinyBip21RawMaterials { @@ -1555,6 +1649,11 @@ impl MutinyWallet { &self, amount: Option, ) -> Result { + let lock = self.node_manager.read().await; + let Some(node_manager) = lock.as_ref() else { + return Err(MutinyError::NodeManagerRequired); + }; + // TODO support more than one federation let federation_ids = self.list_federation_ids().await?; if federation_ids.is_empty() { @@ -1570,10 +1669,7 @@ impl MutinyWallet { // if the user provided amount, this is easy if let Some(amt) = amount { - let (inv, fee) = self - .node_manager - .create_invoice(amt, labels.clone()) - .await?; + let (inv, fee) = node_manager.create_invoice(amt, labels.clone()).await?; let bolt_11 = inv.bolt11.expect("create inv had one job"); self.storage @@ -1602,10 +1698,7 @@ impl MutinyWallet { log_debug!(self.logger, "max spendable: {}", amt); // try to get an invoice for this exact amount - let (inv, fee) = self - .node_manager - .create_invoice(amt, labels.clone()) - .await?; + let (inv, fee) = node_manager.create_invoice(amt, labels.clone()).await?; // check if we can afford that invoice let inv_amt = inv.amount_sats.ok_or(MutinyError::BadAmountError)?; @@ -1618,7 +1711,7 @@ impl MutinyWallet { // if invoice amount changed, create a new invoice let (inv_to_pay, fee) = if first_invoice_amount != inv_amt { - self.node_manager + node_manager .create_invoice(first_invoice_amount, labels.clone()) .await? } else { @@ -1654,6 +1747,11 @@ impl MutinyWallet { &self, amount: Option, ) -> Result, MutinyError> { + let lock = self.node_manager.read().await; + let Some(node_manager) = lock.as_ref() else { + return Err(MutinyError::NodeManagerRequired); + }; + if let Some(0) = amount { return Ok(None); } @@ -1675,7 +1773,7 @@ impl MutinyWallet { if let Some(amt) = amount { // if the user provided amount, this is easy ( - self.node_manager.get_lsp_fee(amt).await?, + node_manager.get_lsp_fee(amt).await?, (calc_routing_fee_msat(amt as f64 * 1_000.0, &fees) / 1_000.0).floor() as u64, ) } else { @@ -1692,10 +1790,7 @@ impl MutinyWallet { log_debug!(self.logger, "max spendable: {}", amt); // try to get an invoice for this exact amount - ( - self.node_manager.get_lsp_fee(amt).await?, - current_balance - amt, - ) + (node_manager.get_lsp_fee(amt).await?, current_balance - amt) } }; @@ -1709,7 +1804,10 @@ impl MutinyWallet { ) -> Result { // Attempt to create federation invoice if available and below max amount let federation_ids = self.list_federation_ids().await?; - if !federation_ids.is_empty() && amount <= MAX_FEDERATION_INVOICE_AMT { + let node_manager = self.node_manager.read().await; + if !federation_ids.is_empty() + && (amount <= MAX_FEDERATION_INVOICE_AMT || node_manager.is_none()) + { let federation_id = &federation_ids[0]; let fedimint_client = self.federations.read().await.get(federation_id).cloned(); @@ -1723,9 +1821,13 @@ impl MutinyWallet { } // Fallback to node_manager invoice creation if no federation invoice created - let (inv, _fee) = self.node_manager.create_invoice(amount, labels).await?; - - Ok(inv) + match node_manager.as_ref() { + Some(nm) => { + let (inv, _) = nm.create_invoice(amount, labels).await?; + Ok(inv) + } + None => Err(MutinyError::NodeManagerRequired), + } } /// Gets the current balance of the wallet. @@ -1733,7 +1835,10 @@ impl MutinyWallet { /// /// This will not include any funds in an unconfirmed lightning channel. pub async fn get_balance(&self) -> Result { - 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?; Ok(MutinyBalance::new(ln_balance, federation_balance)) @@ -1764,7 +1869,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, @@ -1829,10 +1934,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)); + } } } } @@ -1846,7 +1954,7 @@ impl MutinyWallet { &self, label: &String, ) -> Result, MutinyError> { - 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()); }; @@ -1939,8 +2047,10 @@ impl MutinyWallet { hash: &sha256::Hash, ) -> Result { // First, try to find the invoice in the node manager - if let Ok(invoice) = self.node_manager.get_invoice_by_hash(hash).await { - return Ok(invoice); + if let Some(node_manager) = self.node_manager.read().await.as_ref() { + if let Ok(invoice) = node_manager.get_invoice_by_hash(hash).await { + return Ok(invoice); + } } // If not found in node manager, search in federations @@ -2315,7 +2425,19 @@ impl MutinyWallet { /// Stops all of the nodes and background processes. /// Returns after node has been stopped. pub async fn stop(&self) -> Result<(), MutinyError> { - self.node_manager.stop().await + self.stop.store(true, Ordering::Relaxed); + 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) { + log_debug!(self.logger, "stopping storage"); + self.storage.stop(); + log_debug!(self.logger, "stopped storage"); + } + + Ok(()) } pub async fn change_password( @@ -2356,9 +2478,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> { - 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 + utils::sleep(250).await; + } else { + return Err(MutinyError::NodeManagerRequired); + } self.stop().await?; @@ -2367,10 +2493,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?; + } Ok(()) } @@ -3053,8 +3178,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 { @@ -3417,7 +3547,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(); @@ -3427,7 +3557,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] @@ -3440,7 +3570,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(); @@ -3450,11 +3580,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] @@ -3463,13 +3593,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(); @@ -3479,20 +3609,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); + + mw.create_node_manager().await.unwrap(); + let ns = storage.get_nodes().unwrap(); + assert_eq!(ns.version, 1); + assert_eq!(ns.nodes.len(), 1); - assert_eq!(mw.node_manager.list_nodes().await.unwrap().len(), 1); + let lock = mw.node_manager.read().await; + let nm = lock.as_ref().unwrap(); + assert_eq!(nm.list_nodes().await.unwrap().len(), 1); - assert!(mw.node_manager.new_node().await.is_ok()); - // let storage persist - sleep(1000).await; + nm.new_node().await.unwrap(); + let ns = storage.get_nodes().unwrap(); + assert_eq!(ns.nodes.len(), 2); - assert_eq!(mw.node_manager.list_nodes().await.unwrap().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] @@ -3506,7 +3649,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(); @@ -3515,14 +3658,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) @@ -3532,7 +3675,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 @@ -3557,7 +3700,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); } @@ -3573,7 +3716,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(); @@ -3583,12 +3726,12 @@ 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)); let bip21 = mw.create_bip21(None, vec![]).await.unwrap(); assert!(bip21.invoice.is_none()); - let new_node = mw.node_manager.new_node().await; + let new_node = mw.create_node_manager().await; assert!(new_node.is_err()); } @@ -3765,18 +3908,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, @@ -3790,7 +3926,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(), @@ -3801,7 +3937,7 @@ mod tests { input: vec![], output: vec![output.clone()], }; - mw.node_manager + node_manager .wallet .insert_tx( tx1.clone(), @@ -3817,7 +3953,7 @@ mod tests { input: vec![], output: vec![output], }; - mw.node_manager + node_manager .wallet .insert_tx( tx2.clone(), @@ -3955,30 +4091,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 diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index b6977d903..20dd9e817 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -73,6 +73,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 { @@ -88,8 +94,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)] pub struct NodeIdentity { pub uuid: String, pub pubkey: PublicKey, @@ -274,6 +281,7 @@ impl Ord for ChannelClosure { } } +#[derive(Debug, Copy, Clone, Default)] pub struct NodeBalance { pub confirmed: u64, pub unconfirmed: u64, @@ -281,6 +289,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, @@ -480,6 +495,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 { @@ -566,7 +586,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()) } @@ -603,60 +623,27 @@ impl NodeManager { nodes.clear(); log_debug!(self.logger, "stopped all nodes"); - // stop the indexeddb object to close db connection - if self.storage.connected().unwrap_or(false) { - log_debug!(self.logger, "stopping storage"); - self.storage.stop(); - log_debug!(self.logger, "stopped storage"); - } - Ok(()) } /// 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>) { + pub fn start_sync(nm: Arc>>>, stop: Arc) { 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; } // sleep for 1 minute, checking graceful shutdown check each 1s. for _ in 0..60 { - if nm.stop.load(Ordering::Relaxed) { + if stop.load(Ordering::Relaxed) { return; } sleep(1_000).await; @@ -665,6 +652,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> { @@ -681,7 +700,7 @@ impl NodeManager { pub fn get_new_address(&self, labels: Vec) -> Result { 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)?; return Ok(address); } @@ -908,7 +927,7 @@ impl NodeManager { last_seen: utils::now().as_secs(), }); - let address_labels = self.get_address_labels().unwrap_or_default(); + let address_labels = self.storage.get_address_labels().unwrap_or_default(); let labels = address_labels .get(&address.to_string()) .cloned() @@ -986,7 +1005,7 @@ impl NodeManager { pub fn list_onchain(&self) -> Result, MutinyError> { 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)) @@ -999,7 +1018,7 @@ impl NodeManager { pub fn get_transaction(&self, txid: Txid) -> Result, MutinyError> { 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)) } @@ -1231,7 +1250,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 @@ -1251,7 +1269,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; @@ -1897,9 +1914,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(); @@ -1982,7 +2002,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(); @@ -1992,7 +2012,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 d058c7826..f69280847 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(e) => { error!("Got unhandled error: {e}"); // FIXME: For some unknown reason, InsufficientBalance is being returned as `Other` diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 635726442..3ba214931 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. @@ -476,7 +476,7 @@ impl MutinyWallet { /// Returns the network of the wallet. #[wasm_bindgen] pub fn get_network(&self) -> String { - self.inner.node_manager.get_network().to_string() + self.inner.get_network().to_string() } /// Gets a new bitcoin address from the wallet. @@ -484,17 +484,22 @@ impl MutinyWallet { /// /// It is recommended to create a new address for every transaction. #[wasm_bindgen] - pub fn get_new_address( + pub async fn get_new_address( &self, labels: Vec, ) -> Result { - let address = self.inner.node_manager.get_new_address(labels.clone())?; - Ok(MutinyBip21RawMaterials { - address: address.to_string(), - invoice: None, - btc_amount: None, - labels, - }) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + let address = nm.get_new_address(labels.clone())?; + Ok(MutinyBip21RawMaterials { + address: address.to_string(), + invoice: None, + btc_amount: None, + labels, + }) + } else { + // todo add fedimint + Err(MutinyJsError::NodeManagerRequired) + } } /// Creates a BIP 21 invoice. This creates a new address and a lightning invoice. @@ -546,12 +551,15 @@ impl MutinyWallet { fee_rate: Option, ) -> Result { let send_to = Address::from_str(&destination_address)?; - Ok(self - .inner - .node_manager - .send_to_address(send_to, amount, labels, fee_rate) - .await? - .to_string()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm + .send_to_address(send_to, amount, labels, fee_rate) + .await? + .to_string()) + } else { + // todo add fedimint + Err(MutinyJsError::NodeManagerRequired) + } } #[wasm_bindgen] @@ -565,12 +573,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. @@ -585,68 +595,77 @@ impl MutinyWallet { fee_rate: Option, ) -> Result { let send_to = Address::from_str(&destination_address)?; - Ok(self - .inner - .node_manager - .sweep_wallet(send_to, labels, fee_rate) - .await? - .to_string()) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm + .sweep_wallet(send_to, labels, fee_rate) + .await? + .to_string()) + } else { + // todo add fedimint + Err(MutinyJsError::NodeManagerRequired) + } } /// Estimates the onchain fee for a transaction sending to the given address. /// The amount is in satoshis and the fee rate is in sat/vbyte. - pub fn estimate_tx_fee( + pub async fn estimate_tx_fee( &self, destination_address: String, amount: u64, fee_rate: Option, ) -> Result { let addr = Address::from_str(&destination_address)?.assume_checked(); - Ok(self - .inner - .node_manager - .estimate_tx_fee(addr, amount, fee_rate)?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.estimate_tx_fee(addr, amount, fee_rate)?) + } else { + // todo add fedimint + Err(MutinyJsError::NodeManagerRequired) + } } /// Estimates the onchain fee for a transaction sweep our on-chain balance /// to the given address. /// /// The fee rate is in sat/vbyte. - pub fn estimate_sweep_tx_fee( + pub async fn estimate_sweep_tx_fee( &self, destination_address: String, fee_rate: Option, ) -> Result { let addr = Address::from_str(&destination_address)?.assume_checked(); - Ok(self - .inner - .node_manager - .estimate_sweep_tx_fee(addr, fee_rate)?) + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + Ok(nm.estimate_sweep_tx_fee(addr, fee_rate)?) + } else { + // todo add fedimint + Err(MutinyJsError::NodeManagerRequired) + } } /// 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 @@ -673,9 +692,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. @@ -688,21 +710,31 @@ impl MutinyWallet { address: String, ) -> Result */, MutinyJsError> { let address = Address::from_str(&address)?; - Ok(JsValue::from_serde( - &self.inner.node_manager.check_address(address).await?, - )?) + + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + let result = nm.check_address(address).await?; + Ok(JsValue::from_serde(&result)?) + } else { + // todo add fedimint + Err(MutinyJsError::NodeManagerRequired) + } } /// 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.node_manager.get_transaction(txid)?, - )?) + + if let Some(nm) = self.inner.node_manager.read().await.as_ref() { + let result = nm.get_transaction(txid)?; + Ok(JsValue::from_serde(&result)?) + } else { + // todo add fedimint + Err(MutinyJsError::NodeManagerRequired) + } } /// Gets the current balance of the wallet. @@ -716,43 +748,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 @@ -766,9 +840,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. @@ -778,18 +854,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. @@ -798,16 +878,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. @@ -854,12 +945,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. @@ -985,12 +1078,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. @@ -1000,9 +1095,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. @@ -1024,12 +1124,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. @@ -1047,12 +1149,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 @@ -1089,27 +1190,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. @@ -1120,11 +1226,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 @@ -1157,7 +1263,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)?), }; @@ -1284,9 +1390,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 @@ -1298,18 +1402,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 @@ -1321,10 +1420,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> { @@ -1332,7 +1428,6 @@ impl MutinyWallet { Ok(JsValue::from_serde( &self .inner - .node_manager .get_contacts()? .into_iter() .map(|(id, c)| { @@ -1352,7 +1447,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)| { @@ -1375,7 +1469,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)| { @@ -1398,7 +1491,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 @@ -1433,10 +1526,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( @@ -1457,11 +1547,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( @@ -1484,7 +1574,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( @@ -1492,7 +1582,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)) => { @@ -1511,7 +1601,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()) @@ -2018,9 +2107,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(()) } @@ -2175,7 +2267,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, @@ -2203,7 +2295,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 @@ -2240,7 +2332,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(); @@ -2287,7 +2379,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, @@ -2315,7 +2407,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( @@ -2397,7 +2489,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()); @@ -2446,7 +2538,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(); @@ -2525,13 +2617,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 279632cf2..8f07be286 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -556,7 +556,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,