diff --git a/mutiny-core/src/auth.rs b/mutiny-core/src/auth.rs index a3ff3b54b..a4db26595 100644 --- a/mutiny-core/src/auth.rs +++ b/mutiny-core/src/auth.rs @@ -21,8 +21,8 @@ struct CustomClaims { sub: String, } -pub(crate) struct MutinyAuthClient { - auth: AuthManager, +pub struct MutinyAuthClient { + pub auth: AuthManager, lnurl_client: Arc, url: String, http_client: Client, diff --git a/mutiny-core/src/gossip.rs b/mutiny-core/src/gossip.rs index 4d9d9994f..dc8482a62 100644 --- a/mutiny-core/src/gossip.rs +++ b/mutiny-core/src/gossip.rs @@ -104,10 +104,10 @@ fn write_gossip_data( network_graph: &NetworkGraph, ) -> Result<(), MutinyError> { // Save the last sync timestamp - storage.set_data(GOSSIP_SYNC_TIME_KEY, last_sync_timestamp)?; + storage.set_data(GOSSIP_SYNC_TIME_KEY, last_sync_timestamp, None)?; // Save the network graph - storage.set_data(NETWORK_GRAPH_KEY, network_graph.encode().to_hex())?; + storage.set_data(NETWORK_GRAPH_KEY, network_graph.encode().to_hex(), None)?; Ok(()) } @@ -362,7 +362,7 @@ pub(crate) fn save_peer_connection_info( }, }; - storage.set_data(key, new_info)?; + storage.set_data(key, new_info, None)?; Ok(()) } @@ -388,7 +388,7 @@ pub(crate) fn set_peer_label( }, }; - storage.set_data(key, new_info)?; + storage.set_data(key, new_info, None)?; Ok(()) } @@ -406,7 +406,7 @@ pub(crate) fn delete_peer_info( if current.nodes.is_empty() { storage.delete(&[key])?; } else { - storage.set_data(key, current)?; + storage.set_data(key, current, None)?; } } @@ -426,7 +426,7 @@ pub(crate) fn save_ln_peer_info( // if the new info is different than the current info, we should to save it if !current.is_some_and(|c| c == new_info) { - storage.set_data(key, new_info)?; + storage.set_data(key, new_info, None)?; } Ok(()) diff --git a/mutiny-core/src/keymanager.rs b/mutiny-core/src/keymanager.rs index de5e6b40d..4ad555c2c 100644 --- a/mutiny-core/src/keymanager.rs +++ b/mutiny-core/src/keymanager.rs @@ -196,14 +196,12 @@ pub fn generate_seed(num_words: u8) -> Result { // key secret will be derived from `m/0'`. pub(crate) fn create_keys_manager( wallet: Arc>, - mnemonic: &Mnemonic, + xprivkey: ExtendedPrivKey, child_index: u32, logger: Arc, ) -> Result, MutinyError> { let context = Secp256k1::new(); - let seed = mnemonic.to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(wallet.network, &seed)?; let shared_key = xprivkey.derive_priv( &context, &DerivationPath::from(vec![ChildNumber::from_hardened_idx(0)?]), @@ -250,6 +248,7 @@ mod tests { use crate::onchain::OnChainWallet; use crate::storage::MemoryStorage; use bip39::Mnemonic; + use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; use esplora_client::Builder; use std::str::FromStr; @@ -277,10 +276,11 @@ mod tests { logger.clone(), )); let stop = Arc::new(AtomicBool::new(false)); + let xpriv = ExtendedPrivKey::new_master(Network::Testnet, &mnemonic.to_seed("")).unwrap(); let wallet = Arc::new( OnChainWallet::new( - &mnemonic, + xpriv, db, Network::Testnet, esplora, @@ -291,21 +291,21 @@ mod tests { .unwrap(), ); - let km = create_keys_manager(wallet.clone(), &mnemonic, 1, logger.clone()).unwrap(); + let km = create_keys_manager(wallet.clone(), xpriv, 1, logger.clone()).unwrap(); let pubkey = pubkey_from_keys_manager(&km); assert_eq!( "02cae09cf2c8842ace44068a5bf3117a494ebbf69a99e79712483c36f97cdb7b54", pubkey.to_string() ); - let km = create_keys_manager(wallet.clone(), &mnemonic, 2, logger.clone()).unwrap(); + let km = create_keys_manager(wallet.clone(), xpriv, 2, logger.clone()).unwrap(); let second_pubkey = pubkey_from_keys_manager(&km); assert_eq!( "03fcc9eaaf0b84946ea7935e3bc4f2b498893c2f53e5d2994d6877d149601ce553", second_pubkey.to_string() ); - let km = create_keys_manager(wallet, &mnemonic, 2, logger).unwrap(); + let km = create_keys_manager(wallet, xpriv, 2, logger).unwrap(); let second_pubkey_again = pubkey_from_keys_manager(&km); assert_eq!(second_pubkey, second_pubkey_again); diff --git a/mutiny-core/src/labels.rs b/mutiny-core/src/labels.rs index f8816c601..bc668445e 100644 --- a/mutiny-core/src/labels.rs +++ b/mutiny-core/src/labels.rs @@ -121,7 +121,7 @@ impl LabelStorage for S { // update the labels map let mut address_labels = self.get_address_labels()?; address_labels.insert(address.to_string(), labels.clone()); - self.set_data(ADDRESS_LABELS_MAP_KEY, address_labels)?; + self.set_data(ADDRESS_LABELS_MAP_KEY, address_labels, None)?; // update the label items let now = crate::utils::now().as_secs(); @@ -138,7 +138,7 @@ impl LabelStorage for S { // Update the last used timestamp label_item.last_used_time = now; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; } None => { // Create a new label item @@ -147,7 +147,7 @@ impl LabelStorage for S { invoices: vec![], last_used_time: now, }; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; } } } @@ -159,7 +159,7 @@ impl LabelStorage for S { // update the labels map let mut invoice_labels = self.get_invoice_labels()?; invoice_labels.insert(invoice.clone(), labels.clone()); - self.set_data(INVOICE_LABELS_MAP_KEY, invoice_labels)?; + self.set_data(INVOICE_LABELS_MAP_KEY, invoice_labels, None)?; // update the label items let now = crate::utils::now().as_secs(); @@ -176,7 +176,7 @@ impl LabelStorage for S { // Update the last used timestamp label_item.last_used_time = now; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; } None => { // Create a new label item @@ -185,7 +185,7 @@ impl LabelStorage for S { invoices: vec![invoice.to_string()], last_used_time: now, }; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; } } } @@ -224,7 +224,7 @@ impl LabelStorage for S { // convert label into a uuid for uniqueness let id = Uuid::new_v4().to_string(); // create label item - self.set_data(get_label_item_key(&id), current)?; + self.set_data(get_label_item_key(&id), current, None)?; // replace label in address_labels with new uuid let addr_labels = self.get_address_labels()?; @@ -241,7 +241,7 @@ impl LabelStorage for S { updated.insert(addr, new_labels); } } - self.set_data(ADDRESS_LABELS_MAP_KEY, updated)?; + self.set_data(ADDRESS_LABELS_MAP_KEY, updated, None)?; // replace label in invoice_labels with new uuid let invoice_labels = self.get_invoice_labels()?; @@ -258,11 +258,11 @@ impl LabelStorage for S { updated.insert(inv, new_labels); } } - self.set_data(INVOICE_LABELS_MAP_KEY, updated)?; + self.set_data(INVOICE_LABELS_MAP_KEY, updated, None)?; // create the contact let key = get_contact_key(&id); - self.set_data(key, contact)?; + self.set_data(key, contact, None)?; // delete old label item self.delete(&[get_label_item_key(&label)])?; @@ -276,14 +276,14 @@ impl LabelStorage for S { // generate a uuid, this will be the "label" that we use to store the contact let id = Uuid::new_v4().to_string(); let key = get_contact_key(&id); - self.set_data(key, contact)?; + self.set_data(key, contact, None)?; let key = get_label_item_key(&id); let label_item = LabelItem { last_used_time: crate::utils::now().as_secs(), ..Default::default() }; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; Ok(id) } @@ -291,13 +291,13 @@ impl LabelStorage for S { let contact = self.get_contact(&id)?; if let Some(mut contact) = contact { contact.archived = Some(true); - self.set(get_contact_key(&id), contact)?; + self.set(get_contact_key(&id), contact, None)?; } Ok(()) } fn edit_contact(&self, id: impl AsRef, contact: Contact) -> Result<(), MutinyError> { - self.set(get_contact_key(&id), contact) + self.set(get_contact_key(&id), contact, None) } fn get_tag_items(&self) -> Result, MutinyError> { @@ -509,7 +509,7 @@ mod tests { let storage = MemoryStorage::default(); let labels_map = create_test_address_labels_map(); storage - .set_data(ADDRESS_LABELS_MAP_KEY, labels_map.clone()) + .set_data(ADDRESS_LABELS_MAP_KEY, labels_map.clone(), None) .unwrap(); let result = storage.get_address_labels(); @@ -524,7 +524,7 @@ mod tests { let storage = MemoryStorage::default(); let labels_map = create_test_invoice_labels_map(); storage - .set_data(INVOICE_LABELS_MAP_KEY, labels_map.clone()) + .set_data(INVOICE_LABELS_MAP_KEY, labels_map.clone(), None) .unwrap(); let result = storage.get_invoice_labels(); @@ -541,7 +541,7 @@ mod tests { let labels = create_test_labels(); for (label, label_item) in labels.clone() { storage - .set_data(get_label_item_key(label), label_item) + .set_data(get_label_item_key(label), label_item, None) .unwrap(); } @@ -566,7 +566,7 @@ mod tests { let labels = create_test_labels(); for (label, label_item) in labels.clone() { storage - .set_data(get_label_item_key(label), label_item) + .set_data(get_label_item_key(label), label_item, None) .unwrap(); } @@ -618,7 +618,9 @@ mod tests { let contacts = create_test_contacts(); for (id, contact) in contacts.clone() { - storage.set_data(get_contact_key(id), contact).unwrap(); + storage + .set_data(get_contact_key(id), contact, None) + .unwrap(); } let result = storage.get_contacts().unwrap(); @@ -796,7 +798,7 @@ mod tests { let labels = create_test_labels(); for (label, label_item) in labels { storage - .set_data(get_label_item_key(label.clone()), label_item.clone()) + .set_data(get_label_item_key(label.clone()), label_item.clone(), None) .unwrap(); expected_tag_items.push(TagItem::Label((label, label_item))); } diff --git a/mutiny-core/src/ldkstorage.rs b/mutiny-core/src/ldkstorage.rs index 5aaf6b933..afd7d9b45 100644 --- a/mutiny-core/src/ldkstorage.rs +++ b/mutiny-core/src/ldkstorage.rs @@ -87,10 +87,11 @@ impl MutinyNodePersister { &self, key: &str, object: &W, + version: Option, ) -> Result<(), lightning::io::Error> { let key_with_node = self.get_key(key); self.storage - .set_data(key_with_node, object.encode()) + .set_data(key_with_node, object.encode(), version) .map_err(|e| { match e { MutinyError::PersistenceFailed { source } => { @@ -273,7 +274,7 @@ impl MutinyNodePersister { ) -> io::Result<()> { let key = self.get_key(payment_key(inbound, payment_hash).as_str()); self.storage - .set_data(key, payment_info) + .set_data(key, payment_info, None) .map_err(io::Error::other) } @@ -322,7 +323,7 @@ impl MutinyNodePersister { "{CHANNEL_CLOSURE_PREFIX}{}", user_channel_id.to_be_bytes().to_hex() )); - self.storage.set_data(key, closure)?; + self.storage.set_data(key, closure, None)?; Ok(()) } @@ -381,7 +382,7 @@ impl MutinyNodePersister { // add the new descriptors descriptors.extend(failed_hex); - self.storage.set_data(key, descriptors)?; + self.storage.set_data(key, descriptors, None)?; Ok(()) } @@ -422,7 +423,7 @@ impl MutinyNodePersister { params: ChannelOpenParams, ) -> Result<(), MutinyError> { let key = self.get_key(&channel_open_params_key(id)); - self.storage.set_data(key, params) + self.storage.set_data(key, params, None) } pub(crate) fn get_channel_open_params( @@ -516,12 +517,13 @@ impl &self, channel_manager: &PhantomChannelManager, ) -> Result<(), lightning::io::Error> { - self.persist_local_storage(CHANNEL_MANAGER_KEY, channel_manager) + // fixme add version + self.persist_local_storage(CHANNEL_MANAGER_KEY, channel_manager, None) } fn persist_graph(&self, network_graph: &NetworkGraph) -> Result<(), lightning::io::Error> { self.storage - .set_data(NETWORK_GRAPH_KEY, network_graph.encode().to_hex()) + .set_data(NETWORK_GRAPH_KEY, network_graph.encode().to_hex(), None) .map_err(|_| lightning::io::ErrorKind::Other.into()) } @@ -531,7 +533,7 @@ impl ) -> Result<(), lightning::io::Error> { let scorer_str = scorer.encode().to_hex(); self.storage - .set_data(PROB_SCORER_KEY, scorer_str) + .set_data(PROB_SCORER_KEY, scorer_str, None) .map_err(|_| lightning::io::ErrorKind::Other.into()) } } @@ -550,7 +552,15 @@ impl Persist= u32::MAX as u64 { + u32::MAX + } else { + update_id as u32 + }; + + match self.persist_local_storage(&key, monitor, Some(version)) { Ok(()) => chain::ChannelMonitorUpdateStatus::Completed, Err(_) => chain::ChannelMonitorUpdateStatus::PermanentFailure, } @@ -568,7 +578,15 @@ impl Persist= u32::MAX as u64 { + u32::MAX + } else { + update_id as u32 + }; + + match self.persist_local_storage(&key, monitor, Some(version)) { Ok(()) => chain::ChannelMonitorUpdateStatus::Completed, Err(_) => chain::ChannelMonitorUpdateStatus::PermanentFailure, } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index c4aebca4d..60b654f1d 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -10,7 +10,7 @@ // background file is mostly an LDK copy paste mod background; -mod auth; +pub mod auth; mod chain; pub mod encrypt; pub mod error; @@ -21,7 +21,7 @@ mod gossip; mod keymanager; pub mod labels; mod ldkstorage; -mod lnurlauth; +pub mod lnurlauth; pub mod logging; mod lspclient; mod networking; @@ -44,6 +44,7 @@ pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY}; +use crate::auth::MutinyAuthClient; use crate::storage::MutinyStorage; use crate::{error::MutinyError, nostr::ReservedProfile}; use crate::{nodemanager::NodeManager, nostr::ProfileType}; @@ -63,14 +64,14 @@ use std::sync::Arc; #[derive(Clone)] pub struct MutinyWalletConfig { - mnemonic: Option, + xprivkey: ExtendedPrivKey, #[cfg(target_arch = "wasm32")] websocket_proxy_addr: Option, - network: Option, + network: Network, user_esplora_url: Option, user_rgs_url: Option, lsp_url: Option, - auth_url: Option, + auth_client: Option>, subscription_url: Option, do_not_connect_peers: bool, } @@ -78,24 +79,24 @@ pub struct MutinyWalletConfig { impl MutinyWalletConfig { #[allow(clippy::too_many_arguments)] pub fn new( - mnemonic: Option, + xprivkey: ExtendedPrivKey, #[cfg(target_arch = "wasm32")] websocket_proxy_addr: Option, - network: Option, + network: Network, user_esplora_url: Option, user_rgs_url: Option, lsp_url: Option, - auth_url: Option, + auth_client: Option>, subscription_url: Option, ) -> Self { Self { - mnemonic, + xprivkey, #[cfg(target_arch = "wasm32")] websocket_proxy_addr, network, user_esplora_url, user_rgs_url, lsp_url, - auth_url, + auth_client, subscription_url, do_not_connect_peers: false, } @@ -136,9 +137,10 @@ impl MutinyWallet { NodeManager::start_sync(node_manager.clone()); // create nostr manager - let seed = node_manager.show_seed().to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(node_manager.get_network(), &seed)?; - let nostr = Arc::new(NostrManager::from_mnemonic(xprivkey, storage.clone())?); + let nostr = Arc::new(NostrManager::from_mnemonic( + node_manager.xprivkey, + storage.clone(), + )?); let mw = Self { config, @@ -373,9 +375,10 @@ impl MutinyWallet { #[cfg(test)] mod tests { use crate::{ - encrypt::encryption_key_from_pass, nodemanager::NodeManager, MutinyWallet, + encrypt::encryption_key_from_pass, generate_seed, nodemanager::NodeManager, MutinyWallet, MutinyWalletConfig, }; + use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; use crate::test_utils::*; @@ -390,15 +393,17 @@ mod tests { let test_name = "create_mutiny_wallet"; log!("{}", test_name); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &[0; 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)); assert!(!NodeManager::has_node_manager(storage.clone())); let config = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -415,16 +420,17 @@ mod tests { async fn restart_mutiny_wallet() { let test_name = "restart_mutiny_wallet"; log!("{}", test_name); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &[0; 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)); assert!(!NodeManager::has_node_manager(storage.clone())); let config = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -436,11 +442,11 @@ mod tests { .expect("mutiny wallet should initialize"); assert!(NodeManager::has_node_manager(storage)); - let first_seed = mw.node_manager.show_seed(); + let first_seed = mw.node_manager.xprivkey; assert!(mw.stop().await.is_ok()); assert!(mw.start().await.is_ok()); - assert_eq!(first_seed, mw.node_manager.show_seed()); + assert_eq!(first_seed, mw.node_manager.xprivkey); } #[test] @@ -448,16 +454,18 @@ mod tests { let test_name = "restart_mutiny_wallet_with_nodes"; log!("{}", test_name); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &[0; 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)); assert!(!NodeManager::has_node_manager(storage.clone())); let config = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -482,16 +490,18 @@ mod tests { async fn restore_mutiny_mnemonic() { let test_name = "restore_mutiny_mnemonic"; log!("{}", test_name); + let mnemonic = generate_seed(12).unwrap(); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &mnemonic.to_seed("")).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)); assert!(!NodeManager::has_node_manager(storage.clone())); let config = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -501,8 +511,8 @@ mod tests { let mw = MutinyWallet::new(storage.clone(), config) .await .expect("mutiny wallet should initialize"); - let seed = mw.node_manager.show_seed(); - assert_ne!(seed.to_string(), ""); + let seed = mw.node_manager.xprivkey; + assert!(!seed.private_key.is_empty()); // create a second mw and make sure it has a different seed let pass = uuid::Uuid::new_v4().to_string(); @@ -510,10 +520,10 @@ mod tests { let storage2 = MemoryStorage::new(Some(pass), Some(cipher)); assert!(!NodeManager::has_node_manager(storage2.clone())); let config2 = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -523,8 +533,8 @@ mod tests { let mw2 = MutinyWallet::new(storage2.clone(), config2.clone()) .await .expect("mutiny wallet should initialize"); - let seed2 = mw2.node_manager.show_seed(); - assert_ne!(seed.to_string(), seed2.to_string()); + let seed2 = mw2.node_manager.xprivkey; + assert_ne!(seed, seed2); // now restore the first seed into the 2nd mutiny node mw2.stop().await.expect("should stop"); @@ -533,14 +543,14 @@ mod tests { let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage3 = MemoryStorage::new(Some(pass), Some(cipher)); - MutinyWallet::restore_mnemonic(storage3.clone(), seed.clone()) + MutinyWallet::restore_mnemonic(storage3.clone(), mnemonic.clone()) .await .expect("mutiny wallet should restore"); let mw2 = MutinyWallet::new(storage3, config2) .await .expect("mutiny wallet should initialize"); - let restored_seed = mw2.node_manager.show_seed(); - assert_eq!(seed.to_string(), restored_seed.to_string()); + let restored_seed = mw2.node_manager.xprivkey; + assert_eq!(seed, restored_seed); } } diff --git a/mutiny-core/src/logging.rs b/mutiny-core/src/logging.rs index 8f1c22587..96852f641 100644 --- a/mutiny-core/src/logging.rs +++ b/mutiny-core/src/logging.rs @@ -141,7 +141,7 @@ async fn write_logging_data( } // Save the logs - storage.set_data(LOGGING_KEY, &existing_logs)?; + storage.set_data(LOGGING_KEY, &existing_logs, None)?; Ok(()) } diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index 652832c51..3093738c9 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -26,7 +26,6 @@ use crate::{lspclient::FeeRequest, storage::MutinyStorage}; use anyhow::{anyhow, Context}; use bdk::FeeRate; use bdk_esplora::esplora_client::AsyncClient; -use bip39::Mnemonic; use bitcoin::hashes::{hex::ToHex, sha256::Hash as Sha256}; use bitcoin::secp256k1::rand; use bitcoin::{hashes::Hash, secp256k1::PublicKey, BlockHash, Network, OutPoint}; @@ -40,6 +39,7 @@ use lightning::{ util::config::ChannelConfig, }; +use bitcoin::util::bip32::ExtendedPrivKey; use lightning::sign::{EntropySource, InMemorySigner}; use lightning::{ chain::{chainmonitor, Filter, Watch}, @@ -164,7 +164,7 @@ impl Node { pub(crate) async fn new( uuid: String, node_index: &NodeIndex, - mnemonic: &Mnemonic, + xprivkey: ExtendedPrivKey, storage: S, gossip_sync: Arc, scorer: Arc>, @@ -186,7 +186,7 @@ impl Node { let keys_manager = Arc::new(create_keys_manager( wallet.clone(), - mnemonic, + xprivkey, node_index.child_index, logger.clone(), )?); diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index cda201560..20d6e21d6 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -3,6 +3,7 @@ use lightning::sign::{NodeSigner, Recipient}; use std::sync::atomic::{AtomicBool, Ordering}; use std::{collections::HashMap, ops::Deref, sync::Arc}; +use crate::gossip::*; use crate::lnurlauth::AuthManager; use crate::logging::LOGGING_KEY; use crate::redshift::{RedshiftManager, RedshiftStatus, RedshiftStorage}; @@ -13,13 +14,12 @@ use crate::scb::{ use crate::storage::{MutinyStorage, KEYCHAIN_STORE_KEY}; use crate::utils::sleep; use crate::MutinyWalletConfig; -use crate::{auth::MutinyAuthClient, gossip::*}; use crate::{ chain::MutinyChain, error::MutinyError, esplora::EsploraSyncClient, fees::MutinyFeeEstimator, - gossip, keymanager, + gossip, logging::MutinyLogger, lspclient::LspClient, node::{Node, ProbScorer, PubkeyConnectionInfo, RapidGossipSync}, @@ -35,7 +35,6 @@ use crate::{labels::LabelStorage, subscription::MutinySubscriptionClient}; use bdk::chain::{BlockId, ConfirmationTime}; use bdk::{wallet::AddressIndex, LocalUtxo}; use bdk_esplora::esplora_client::AsyncClient; -use bip39::Mnemonic; use bitcoin::blockdata::script; use bitcoin::hashes::hex::ToHex; use bitcoin::hashes::{sha256, Hash}; @@ -500,7 +499,7 @@ pub struct Plan { /// services provided by Mutiny. pub struct NodeManager { pub(crate) stop: Arc, - mnemonic: Mnemonic, + pub(crate) xprivkey: ExtendedPrivKey, network: Network, #[cfg(target_arch = "wasm32")] websocket_proxy_addr: String, @@ -540,26 +539,9 @@ impl NodeManager { .websocket_proxy_addr .unwrap_or_else(|| String::from("wss://p.mutinywallet.com")); - let network: Network = c.network.unwrap_or(Network::Bitcoin); - - let mnemonic = match c.mnemonic { - Some(seed) => storage.insert_mnemonic(seed)?, - None => match storage.get_mnemonic() { - Ok(Some(mnemonic)) => mnemonic, - Ok(None) => { - let seed = keymanager::generate_seed(12)?; - storage.insert_mnemonic(seed)? - } - Err(_) => { - // if we get an error, then we have the wrong password - return Err(MutinyError::IncorrectPassword); - } - }, - }; - let logger = Arc::new(MutinyLogger::with_writer(stop.clone(), storage.clone())); - let esplora_server_url = get_esplora_url(network, c.user_esplora_url); + let esplora_server_url = get_esplora_url(c.network, c.user_esplora_url); let tx_sync = Arc::new(EsploraSyncClient::new(esplora_server_url, logger.clone())); let esplora = Arc::new(tx_sync.client().clone()); @@ -570,9 +552,9 @@ impl NodeManager { )); let wallet = Arc::new(OnChainWallet::new( - &mnemonic, + c.xprivkey, storage.clone(), - network, + c.network, esplora.clone(), fee_estimator.clone(), stop.clone(), @@ -582,7 +564,7 @@ impl NodeManager { let chain = Arc::new(MutinyChain::new(tx_sync, wallet.clone(), logger.clone())); let (gossip_sync, scorer) = - gossip::get_gossip_sync(&storage, c.user_rgs_url, network, logger.clone()).await?; + get_gossip_sync(&storage, c.user_rgs_url, c.network, logger.clone()).await?; let scorer = Arc::new(utils::Mutex::new(scorer)); @@ -627,14 +609,14 @@ impl NodeManager { let node = Node::new( node_item.0, &node_item.1, - &mnemonic, + c.xprivkey, storage.clone(), gossip_sync.clone(), scorer.clone(), chain.clone(), fee_estimator.clone(), wallet.clone(), - network, + c.network, esplora.clone(), &lsp_clients, logger.clone(), @@ -671,47 +653,33 @@ impl NodeManager { let nodes = Arc::new(Mutex::new(nodes_map)); - let seed = mnemonic.to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(network, &seed)?; - let auth = AuthManager::new(xprivkey)?; - let lnurl_client = Arc::new( lnurl::Builder::default() .build_async() .expect("failed to make lnurl client"), ); - let auth_client = if let Some(auth_url) = c.auth_url { - let a = Arc::new(MutinyAuthClient::new( - auth.clone(), - lnurl_client.clone(), - logger.clone(), - auth_url, - )); - Some(a) - } else { - None - }; - - let subscription_client = if let Some(auth_client) = auth_client { + let (subscription_client, auth) = if let Some(auth_client) = c.auth_client { if let Some(subscription_url) = c.subscription_url { + let auth = auth_client.auth.clone(); let s = Arc::new(MutinySubscriptionClient::new( auth_client, subscription_url, logger.clone(), )); - Some(s) + (Some(s), auth) } else { - None + (None, auth_client.auth.clone()) } } else { - None + let auth_manager = AuthManager::new(c.xprivkey)?; + (None, auth_manager) }; let nm = NodeManager { stop, - mnemonic, - network, + xprivkey: c.xprivkey, + network: c.network, wallet, gossip_sync, scorer, @@ -894,11 +862,6 @@ impl NodeManager { self.wallet.broadcast_transaction(tx).await } - /// Returns the mnemonic seed phrase for the wallet. - pub fn show_seed(&self) -> Mnemonic { - self.mnemonic.clone() - } - /// Returns the network of the wallet. pub fn get_network(&self) -> Network { self.network @@ -1933,12 +1896,13 @@ impl NodeManager { } fn get_scb_key(&self) -> SecretKey { - let seed = self.mnemonic.to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(self.network, &seed).unwrap(); let path = DerivationPath::from_str(SCB_ENCRYPTION_KEY_DERIVATION_PATH).unwrap(); let context = Secp256k1::new(); - xprivkey.derive_priv(&context, &path).unwrap().private_key + self.xprivkey + .derive_priv(&context, &path) + .unwrap() + .private_key } /// Creates a static channel backup for all the nodes in the node manager. @@ -2020,7 +1984,7 @@ impl NodeManager { let new_node = Node::new( uuid, &node_index, - &self.mnemonic, + self.xprivkey, self.storage.clone(), self.gossip_sync.clone(), self.scorer.clone(), @@ -2348,7 +2312,7 @@ pub(crate) async fn create_new_node_from_node_manager( let new_node_res = Node::new( next_node_uuid.clone(), &next_node, - &node_manager.mnemonic, + node_manager.xprivkey, node_manager.storage.clone(), node_manager.gossip_sync.clone(), node_manager.scorer.clone(), @@ -2398,6 +2362,7 @@ mod tests { use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::PublicKey; + use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{Network, PackedLockTime, Transaction, TxOut, Txid}; use lightning::ln::PaymentHash; use lightning_invoice::Invoice; @@ -2406,7 +2371,7 @@ mod tests { use crate::test_utils::*; use crate::event::{HTLCStatus, MillisatAmount, PaymentInfo}; - use crate::storage::MemoryStorage; + use crate::storage::{MemoryStorage, MutinyStorage}; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; wasm_bindgen_test_configure!(run_in_browser); @@ -2417,6 +2382,8 @@ mod tests { async fn create_node_manager() { let test_name = "create_node_manager"; log!("{}", test_name); + let seed = generate_seed(12).unwrap(); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &seed.to_seed("")).unwrap(); let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); @@ -2424,10 +2391,10 @@ mod tests { assert!(!NodeManager::has_node_manager(storage.clone())); let c = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -2437,31 +2404,10 @@ mod tests { NodeManager::new(c, storage.clone()) .await .expect("node manager should initialize"); + storage.insert_mnemonic(seed).unwrap(); assert!(NodeManager::has_node_manager(storage)); } - #[test] - async fn correctly_show_seed() { - let test_name = "correctly_show_seed"; - log!("{}", test_name); - - let seed = generate_seed(12).expect("Failed to gen seed"); - let c = MutinyWalletConfig::new( - Some(seed.clone()), - #[cfg(target_arch = "wasm32")] - None, - Some(Network::Regtest), - None, - None, - None, - None, - None, - ); - let nm = NodeManager::new(c, ()).await.unwrap(); - - assert_eq!(seed, nm.show_seed()); - } - #[test] async fn created_new_nodes() { let test_name = "created_new_nodes"; @@ -2471,11 +2417,12 @@ mod tests { let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher)); let seed = generate_seed(12).expect("Failed to gen seed"); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &seed.to_seed("")).unwrap(); let c = MutinyWalletConfig::new( - Some(seed), + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -2519,11 +2466,12 @@ mod tests { let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher)); let seed = generate_seed(12).expect("Failed to gen seed"); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &seed.to_seed("")).unwrap(); let c = MutinyWalletConfig::new( - Some(seed), + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Signet), + Network::Signet, None, None, None, diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 1ba03c938..868d3e0b0 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -132,7 +132,7 @@ impl NostrManager { .iter() .map(|x| x.profile.clone()) .collect::>(); - self.storage.set_data(NWC_STORAGE_KEY, profiles)?; + self.storage.set_data(NWC_STORAGE_KEY, profiles, None)?; } Ok(nwc_profile) @@ -181,7 +181,7 @@ impl NostrManager { .iter() .map(|x| x.profile.clone()) .collect::>(); - self.storage.set_data(NWC_STORAGE_KEY, profiles)?; + self.storage.set_data(NWC_STORAGE_KEY, profiles, None)?; } Ok(nwc.nwc_profile()) @@ -302,7 +302,8 @@ impl NostrManager { // remove from storage pending.retain(|x| x.invoice.payment_hash() != &hash); - self.storage.set_data(PENDING_NWC_EVENTS_KEY, pending)?; + self.storage + .set_data(PENDING_NWC_EVENTS_KEY, pending, None)?; Ok(event_id) } @@ -323,7 +324,8 @@ impl NostrManager { // remove the invoice invoices.retain(|x| x.invoice.payment_hash() != hash); - self.storage.set_data(PENDING_NWC_EVENTS_KEY, invoices)?; + self.storage + .set_data(PENDING_NWC_EVENTS_KEY, invoices, None)?; Ok(()) } @@ -338,7 +340,8 @@ impl NostrManager { // remove expired invoices invoices.retain(|x| !x.is_expired()); - self.storage.set_data(PENDING_NWC_EVENTS_KEY, invoices)?; + self.storage + .set_data(PENDING_NWC_EVENTS_KEY, invoices, None)?; Ok(()) } @@ -621,7 +624,7 @@ mod test { // add dummy to storage nostr_manager .storage - .set_data(PENDING_NWC_EVENTS_KEY, vec![inv.clone()]) + .set_data(PENDING_NWC_EVENTS_KEY, vec![inv.clone()], None) .unwrap(); let pending = nostr_manager.get_pending_nwc_invoices().unwrap(); diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index 483c4875f..c1b3f405b 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -181,7 +181,7 @@ impl NostrWalletConnect { node_manager .storage - .set_data(PENDING_NWC_EVENTS_KEY, current)?; + .set_data(PENDING_NWC_EVENTS_KEY, current, None)?; return Ok(None); } else { diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index c030a50e9..0a6b19495 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -9,7 +9,6 @@ use bdk::psbt::PsbtUtils; use bdk::template::DescriptorTemplateOut; use bdk::{FeeRate, LocalUtxo, SignOptions, TransactionDetails, Wallet}; use bdk_esplora::{esplora_client, EsploraAsyncExt}; -use bip39::Mnemonic; use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}; use bitcoin::{Address, Network, OutPoint, Script, Transaction, Txid}; @@ -38,7 +37,7 @@ pub struct OnChainWallet { impl OnChainWallet { pub fn new( - mnemonic: &Mnemonic, + xprivkey: ExtendedPrivKey, db: S, network: Network, esplora: Arc, @@ -46,8 +45,6 @@ impl OnChainWallet { stop: Arc, logger: Arc, ) -> Result, MutinyError> { - let seed = mnemonic.to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(network, &seed)?; let account_number = 0; let (receive_descriptor_template, change_descriptor_template) = get_tr_descriptors_for_extended_key(xprivkey, network, account_number)?; @@ -555,6 +552,7 @@ mod tests { use super::*; use crate::test_utils::*; use crate::{encrypt::encryption_key_from_pass, storage::MemoryStorage}; + use bip39::Mnemonic; use bitcoin::Address; use esplora_client::Builder; use std::str::FromStr; @@ -578,8 +576,9 @@ mod tests { logger.clone(), )); let stop = Arc::new(AtomicBool::new(false)); + let xpriv = ExtendedPrivKey::new_master(Network::Testnet, &mnemonic.to_seed("")).unwrap(); - OnChainWallet::new(&mnemonic, db, Network::Testnet, esplora, fees, stop, logger).unwrap() + OnChainWallet::new(xpriv, db, Network::Testnet, esplora, fees, stop, logger).unwrap() } #[test] diff --git a/mutiny-core/src/redshift.rs b/mutiny-core/src/redshift.rs index 42b1d7e34..83ab09fcc 100644 --- a/mutiny-core/src/redshift.rs +++ b/mutiny-core/src/redshift.rs @@ -126,7 +126,7 @@ impl RedshiftStorage for S { } fn persist_redshift(&self, redshift: Redshift) -> Result<(), MutinyError> { - self.set_data(get_redshift_key(&redshift.id), redshift) + self.set_data(get_redshift_key(&redshift.id), redshift, None) } } diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 977ee97ea..9a8ce8ba6 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -67,12 +67,22 @@ pub trait MutinyStorage: Clone + Sized + 'static { fn cipher(&self) -> Option; /// Set a value in the storage, the value will already be encrypted if needed - fn set(&self, key: impl AsRef, value: T) -> Result<(), MutinyError> + fn set( + &self, + key: impl AsRef, + value: T, + version: Option, + ) -> Result<(), MutinyError> where T: Serialize; /// Set a value in the storage, the function will encrypt the value if needed - fn set_data(&self, key: impl AsRef, value: T) -> Result<(), MutinyError> + fn set_data( + &self, + key: impl AsRef, + value: T, + version: Option, + ) -> Result<(), MutinyError> where T: Serialize, { @@ -82,7 +92,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { let json: Value = encrypt_value(key.as_ref(), data, self.cipher())?; - self.set(key, json) + self.set(key, json, version) } /// Get a value from the storage, use get_data if you want the value to be decrypted @@ -141,7 +151,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { /// Insert a mnemonic into the storage fn insert_mnemonic(&self, mnemonic: Mnemonic) -> Result { - self.set_data(MNEMONIC_KEY, &mnemonic)?; + self.set_data(MNEMONIC_KEY, &mnemonic, None)?; Ok(mnemonic) } @@ -190,7 +200,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { // encrypt all of the values for (key, value) in values.iter() { - self.set_data(key, value)?; + self.set_data(key, value, None)?; } Ok(()) @@ -213,7 +223,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { /// Inserts the node indexes into storage fn insert_nodes(&self, nodes: NodeStorage) -> Result<(), MutinyError> { - self.set_data(NODES_KEY, nodes) + self.set_data(NODES_KEY, nodes, None) } /// Get the current fee estimates from storage @@ -225,7 +235,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { /// Inserts the fee estimates into storage /// The key is block target, the value is the fee in satoshis per byte fn insert_fee_estimates(&self, fees: HashMap) -> Result<(), MutinyError> { - self.set_data(FEE_ESTIMATES_KEY, fees) + self.set_data(FEE_ESTIMATES_KEY, fees, None) } fn has_done_first_sync(&self) -> Result { @@ -234,7 +244,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { } fn set_done_first_sync(&self) -> Result<(), MutinyError> { - self.set_data(FIRST_SYNC_KEY, true) + self.set_data(FIRST_SYNC_KEY, true, None) } } @@ -270,7 +280,12 @@ impl MutinyStorage for MemoryStorage { self.cipher.to_owned() } - fn set(&self, key: impl AsRef, value: T) -> Result<(), MutinyError> + fn set( + &self, + key: impl AsRef, + value: T, + _version: Option, + ) -> Result<(), MutinyError> where T: Serialize, { @@ -374,7 +389,12 @@ impl MutinyStorage for () { None } - fn set(&self, _key: impl AsRef, _value: T) -> Result<(), MutinyError> + fn set( + &self, + _key: impl AsRef, + _value: T, + _version: Option, + ) -> Result<(), MutinyError> where T: Serialize, { @@ -441,9 +461,9 @@ where match self.0.get_data::(KEYCHAIN_STORE_KEY)? { Some(mut keychain_store) => { keychain_store.append(changeset.clone()); - self.0.set_data(KEYCHAIN_STORE_KEY, keychain_store) + self.0.set_data(KEYCHAIN_STORE_KEY, keychain_store, None) } - None => self.0.set_data(KEYCHAIN_STORE_KEY, changeset), + None => self.0.set_data(KEYCHAIN_STORE_KEY, changeset, None), } } diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index f3bd995ec..ce55ecbae 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -81,7 +81,7 @@ impl EncryptedVssKeyValueItem { } impl MutinyVssClient { - pub(crate) fn new( + pub fn new( auth_client: Arc, url: String, encryption_key: SecretKey, diff --git a/mutiny-wasm/Cargo.toml b/mutiny-wasm/Cargo.toml index a7487aab9..67e9e4a1e 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -38,6 +38,7 @@ gloo-utils = { version = "0.1.6", features = ["serde"] } web-sys = { version = "0.3.60", features = ["console"] } bip39 = { version = "2.0.0" } getrandom = { version = "0.2", features = ["js"] } +futures = "0.3.25" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/mutiny-wasm/src/indexed_db.rs b/mutiny-wasm/src/indexed_db.rs index b2577bd7f..f845848b9 100644 --- a/mutiny-wasm/src/indexed_db.rs +++ b/mutiny-wasm/src/indexed_db.rs @@ -2,10 +2,11 @@ use anyhow::anyhow; use gloo_storage::{LocalStorage, Storage}; use gloo_utils::format::JsValueSerdeExt; use lightning::util::logger::Logger; -use lightning::{log_debug, log_error}; +use lightning::{log_debug, log_error, log_trace}; use log::error; use mutiny_core::logging::MutinyLogger; use mutiny_core::storage::{MutinyStorage, KEYCHAIN_STORE_KEY}; +use mutiny_core::vss::*; use mutiny_core::*; use mutiny_core::{ encrypt::Cipher, @@ -31,6 +32,7 @@ pub struct IndexedDbStorage { /// This is a RwLock because we want to be able to read from it without blocking memory: Arc>>, pub(crate) indexed_db: Arc>>, + vss: Option>, logger: Arc, } @@ -38,11 +40,12 @@ impl IndexedDbStorage { pub async fn new( password: Option, cipher: Option, + vss: Option>, logger: Arc, ) -> Result { let indexed_db = Arc::new(RwLock::new(Some(Self::build_indexed_db_database().await?))); - let map = Self::read_all(&indexed_db, &logger).await?; + let map = Self::read_all(&indexed_db, vss.as_deref(), &logger).await?; let memory = Arc::new(RwLock::new(map)); let password = password.filter(|p| !p.is_empty()); @@ -51,6 +54,7 @@ impl IndexedDbStorage { cipher, memory, indexed_db, + vss, logger, }) } @@ -142,6 +146,7 @@ impl IndexedDbStorage { pub(crate) async fn read_all( indexed_db: &Arc>>, + vss: Option<&MutinyVssClient>, logger: &MutinyLogger, ) -> Result, MutinyError> { let store = { @@ -199,7 +204,46 @@ impl IndexedDbStorage { } } - Ok(map) + match vss { + None => Ok(map), + Some(vss) => { + log_debug!(logger, "Reading from vss"); + let keys = vss.list_key_versions(None).await?; + let mut vss_map = HashMap::new(); + // todo do this in parallel + // todo only get keys we want (probably will filter out scb) + for key in keys { + let obj = vss.get_object(&key.key).await?; + if let Some(value) = obj.value { + log_trace!( + logger, + "Found vss key {} with version {}", + key.key, + key.version + ); + + // we can get versions from monitors, so we should compare + if key.key.contains("monitor") { + if let Some(current) = map.get(&key.key) { + let bytes: Vec = serde_json::from_value(current.clone())?; + // check first byte is 1, then take u64 from next 8 bytes + let current_version = + u64::from_be_bytes(bytes[1..9].try_into().unwrap()); + // if the current version is less than the version from vss, then we want to use the vss version + if current_version < key.version as u64 { + vss_map.insert(key.key, value); + } + continue; + } + } + + vss_map.insert(key.key, value); + } + } + map.extend(vss_map); + Ok(map) + } + } } async fn build_indexed_db_database() -> Result { @@ -217,7 +261,7 @@ impl IndexedDbStorage { #[cfg(test)] pub(crate) async fn reload_from_indexed_db(&self) -> Result<(), MutinyError> { - let map = Self::read_all(&self.indexed_db, &self.logger).await?; + let map = Self::read_all(&self.indexed_db, self.vss.as_deref(), &self.logger).await?; let mut memory = self .memory .try_write() @@ -259,7 +303,12 @@ impl MutinyStorage for IndexedDbStorage { self.cipher.to_owned() } - fn set(&self, key: impl AsRef, value: T) -> Result<(), MutinyError> + fn set( + &self, + key: impl AsRef, + value: T, + version: Option, + ) -> Result<(), MutinyError> where T: Serialize, { @@ -272,10 +321,30 @@ impl MutinyStorage for IndexedDbStorage { let key_clone = key.clone(); let data_clone = data.clone(); let logger = self.logger.clone(); + let vss = self.vss.clone(); spawn_local(async move { - if let Err(e) = Self::save_to_indexed_db(&indexed_db, &key_clone, &data_clone).await { + if let (Some(vss), Some(version)) = (vss, version) { + let item = VssKeyValueItem { + key: key_clone.clone(), + value: Some(data_clone.clone()), + version, + }; + let vss_fut = vss.put_objects(vec![item]); + let db_fut = Self::save_to_indexed_db(&indexed_db, &key_clone, &data_clone); + let (vss_result, db_result) = futures::join!(vss_fut, db_fut); + + if let Err(e) = vss_result { + log_error!(logger, "Failed to save ({key_clone}) to vss: {e}"); + } + + if let Err(e) = db_result { + log_error!(logger, "Failed to save ({key_clone}) to indexed db: {e}"); + } + } else if let Err(e) = + Self::save_to_indexed_db(&indexed_db, &key_clone, &data_clone).await + { log_error!(logger, "Failed to save ({key_clone}) to indexed db: {e}"); - } + }; }); // Some values we want to write to local storage as well as indexed db @@ -370,7 +439,7 @@ impl MutinyStorage for IndexedDbStorage { self.indexed_db.clone() }; - let map = Self::read_all(&indexed_db, &self.logger).await?; + let map = Self::read_all(&indexed_db, self.vss.as_deref(), &self.logger).await?; let memory = Arc::new(RwLock::new(map)); self.indexed_db = indexed_db; self.memory = memory; @@ -496,7 +565,7 @@ mod tests { log!("{test_name}"); let logger = Arc::new(MutinyLogger::default()); - let storage = IndexedDbStorage::new(Some("".to_string()), None, logger) + let storage = IndexedDbStorage::new(Some("".to_string()), None, None, logger) .await .unwrap(); @@ -514,14 +583,14 @@ mod tests { let logger = Arc::new(MutinyLogger::default()); let password = "password".to_string(); let cipher = encryption_key_from_pass(&password).unwrap(); - let storage = IndexedDbStorage::new(Some(password), Some(cipher), logger) + let storage = IndexedDbStorage::new(Some(password), Some(cipher), None, logger) .await .unwrap(); let result: Option = storage.get(key).unwrap(); assert_eq!(result, None); - storage.set(key, value).unwrap(); + storage.set(key, value, None).unwrap(); let result: Option = storage.get(key).unwrap(); assert_eq!(result, Some(value.to_string())); @@ -566,7 +635,7 @@ mod tests { let logger = Arc::new(MutinyLogger::default()); let password = "password".to_string(); let cipher = encryption_key_from_pass(&password).unwrap(); - let storage = IndexedDbStorage::new(Some(password), Some(cipher), logger) + let storage = IndexedDbStorage::new(Some(password), Some(cipher), None, logger) .await .unwrap(); @@ -591,11 +660,11 @@ mod tests { let logger = Arc::new(MutinyLogger::default()); let password = "password".to_string(); let cipher = encryption_key_from_pass(&password).unwrap(); - let storage = IndexedDbStorage::new(Some(password), Some(cipher), logger) + let storage = IndexedDbStorage::new(Some(password), Some(cipher), None, logger) .await .unwrap(); - storage.set(key, value).unwrap(); + storage.set(key, value, None).unwrap(); IndexedDbStorage::clear().await.unwrap(); @@ -616,7 +685,9 @@ mod tests { let seed = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").expect("could not generate"); let logger = Arc::new(MutinyLogger::default()); - let storage = IndexedDbStorage::new(None, None, logger).await.unwrap(); + let storage = IndexedDbStorage::new(None, None, None, logger) + .await + .unwrap(); let mnemonic = storage.insert_mnemonic(seed).unwrap(); let stored_mnemonic = storage.get_mnemonic().unwrap(); @@ -636,7 +707,7 @@ mod tests { let logger = Arc::new(MutinyLogger::default()); let password = "password".to_string(); let cipher = encryption_key_from_pass(&password).unwrap(); - let storage = IndexedDbStorage::new(Some(password), Some(cipher), logger) + let storage = IndexedDbStorage::new(Some(password), Some(cipher), None, logger) .await .unwrap(); diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 33fbcffa8..181435571 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -18,18 +18,22 @@ use bitcoin::consensus::deserialize; use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::sha256; use bitcoin::secp256k1::PublicKey; +use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{Address, Network, OutPoint, Transaction, Txid}; use gloo_utils::format::JsValueSerdeExt; use lightning::routing::gossip::NodeId; use lightning_invoice::Invoice; use lnurl::lnurl::LnUrl; +use mutiny_core::auth::MutinyAuthClient; +use mutiny_core::lnurlauth::AuthManager; use mutiny_core::redshift::RedshiftManager; +use mutiny_core::redshift::RedshiftRecipient; use mutiny_core::scb::EncryptedSCB; use mutiny_core::storage::MutinyStorage; -use mutiny_core::{encrypt::encryption_key_from_pass, nostr::nwc::NwcProfile}; +use mutiny_core::vss::MutinyVssClient; +use mutiny_core::{encrypt::encryption_key_from_pass, generate_seed, nostr::nwc::NwcProfile}; use mutiny_core::{labels::LabelStorage, nodemanager::NodeManager}; use mutiny_core::{logging::MutinyLogger, nostr::ProfileType}; -use mutiny_core::{nodemanager, redshift::RedshiftRecipient}; use std::str::FromStr; use std::sync::Arc; use std::{ @@ -40,6 +44,7 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct MutinyWallet { + mnemonic: Mnemonic, inner: mutiny_core::MutinyWallet, } @@ -67,33 +72,86 @@ impl MutinyWallet { lsp_url: Option, auth_url: Option, subscription_url: Option, + storage_url: Option, do_not_connect_peers: Option, ) -> Result { utils::set_panic_hook(); - - let network: Option = network_str.map(|s| s.parse().expect("Invalid network")); - - let mnemonic = match mnemonic_str { - Some(m) => Some(Mnemonic::from_str(&m).map_err(|_| MutinyJsError::InvalidMnemonic)?), - None => None, - }; - let logger = Arc::new(MutinyLogger::default()); + let cipher = password .as_ref() .filter(|p| !p.is_empty()) .map(|p| encryption_key_from_pass(p)) .transpose()?; - let storage = IndexedDbStorage::new(password, cipher, logger).await?; + + let network: Network = network_str + .map(|s| s.parse().expect("Invalid network")) + .unwrap_or(Network::Bitcoin); + + let storage = + IndexedDbStorage::new(password.clone(), cipher.clone(), None, logger.clone()).await?; + + let mnemonic = match mnemonic_str { + Some(m) => { + let seed = Mnemonic::from_str(&m).map_err(|_| MutinyJsError::InvalidMnemonic)?; + storage.insert_mnemonic(seed)? + } + None => match storage.get_mnemonic() { + Ok(Some(mnemonic)) => mnemonic, + Ok(None) => { + let seed = generate_seed(12)?; + storage.insert_mnemonic(seed)? + } + Err(_) => { + // if we get an error, then we have the wrong password + return Err(MutinyJsError::IncorrectPassword); + } + }, + }; + + let seed = mnemonic.to_seed(""); + let xprivkey = ExtendedPrivKey::new_master(network, &seed).unwrap(); + + let (auth_client, vss_client) = if let Some(auth_url) = auth_url.clone() { + let auth_manager = AuthManager::new(xprivkey).unwrap(); + + let lnurl_client = Arc::new( + lnurl::Builder::default() + .build_async() + .expect("failed to make lnurl client"), + ); + + let auth_client = Arc::new(MutinyAuthClient::new( + auth_manager, + lnurl_client, + logger.clone(), + auth_url, + )); + + let vss = storage_url.map(|url| { + Arc::new(MutinyVssClient::new( + auth_client.clone(), + url, + xprivkey.private_key, + logger.clone(), + )) + }); + + (Some(auth_client), vss) + } else { + (None, None) + }; + + let storage = IndexedDbStorage::new(password, cipher, vss_client, logger.clone()).await?; let mut config = mutiny_core::MutinyWalletConfig::new( - mnemonic, + xprivkey, websocket_proxy_addr, network, user_esplora_url, user_rgs_url, lsp_url, - auth_url, + auth_client, subscription_url, ); @@ -102,7 +160,7 @@ impl MutinyWallet { } let inner = mutiny_core::MutinyWallet::new(storage, config).await?; - Ok(MutinyWallet { inner }) + Ok(MutinyWallet { mnemonic, inner }) } /// Returns if there is a saved wallet in storage. @@ -119,10 +177,10 @@ impl MutinyWallet { Ok(c) => c, Err(_) => return false, }; - let storage = IndexedDbStorage::new(password, cipher, logger) + let storage = IndexedDbStorage::new(password, cipher, None, logger) .await .expect("Failed to init"); - nodemanager::NodeManager::has_node_manager(storage) + NodeManager::has_node_manager(storage) } /// Starts up all the nodes again. @@ -153,7 +211,7 @@ impl MutinyWallet { /// Returns the mnemonic seed phrase for the wallet. #[wasm_bindgen] pub fn show_seed(&self) -> String { - self.inner.node_manager.show_seed().to_string() + self.mnemonic.to_string() } /// Returns the network of the wallet. @@ -972,7 +1030,7 @@ impl MutinyWallet { pub async fn get_logs() -> Result> */, MutinyJsError> { let logger = Arc::new(MutinyLogger::default()); // Password should not be required for logs - let storage = IndexedDbStorage::new(None, None, logger.clone()).await?; + let storage = IndexedDbStorage::new(None, None, None, logger.clone()).await?; let stop = Arc::new(AtomicBool::new(false)); let logger = Arc::new(MutinyLogger::with_writer(stop.clone(), storage.clone())); let res = JsValue::from_serde(&NodeManager::get_logs(storage, logger)?)?; @@ -1123,7 +1181,8 @@ impl MutinyWallet { .filter(|p| !p.is_empty()) .map(|p| encryption_key_from_pass(p)) .transpose()?; - let storage = IndexedDbStorage::new(password, cipher, logger).await?; + // todo init vss + let storage = IndexedDbStorage::new(password, cipher, None, logger).await?; if storage.get_mnemonic().is_err() { // if we get an error, then we have the wrong password return Err(MutinyJsError::IncorrectPassword); @@ -1155,7 +1214,7 @@ impl MutinyWallet { .filter(|p| !p.is_empty()) .map(|p| encryption_key_from_pass(p)) .transpose()?; - let storage = IndexedDbStorage::new(password, cipher, logger).await?; + let storage = IndexedDbStorage::new(password, cipher, None, logger).await?; mutiny_core::MutinyWallet::::restore_mnemonic( storage, Mnemonic::from_str(&m).map_err(|_| MutinyJsError::InvalidMnemonic)?, @@ -1226,6 +1285,7 @@ mod tests { None, None, None, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1256,6 +1316,7 @@ mod tests { None, None, None, + None, ) .await .unwrap(); @@ -1289,6 +1350,7 @@ mod tests { None, None, None, + None, ) .await .expect("mutiny wallet should initialize");