From 7cc8f5fe1446b995eb7325cf0d5b535f11349141 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Mon, 27 Nov 2023 15:41:16 -0600 Subject: [PATCH 01/12] Refactor nodemanager methods and pubkey reliance --- mutiny-core/src/key.rs | 70 +++++++ mutiny-core/src/keymanager.rs | 9 +- mutiny-core/src/lib.rs | 287 +++++++++++++++++++++++++- mutiny-core/src/node.rs | 11 +- mutiny-core/src/nodemanager.rs | 359 +++++++-------------------------- mutiny-core/src/nostr/mod.rs | 5 +- mutiny-core/src/redshift.rs | 4 +- mutiny-wasm/src/lib.rs | 80 ++------ mutiny-wasm/src/models.rs | 26 +-- 9 files changed, 455 insertions(+), 396 deletions(-) create mode 100644 mutiny-core/src/key.rs diff --git a/mutiny-core/src/key.rs b/mutiny-core/src/key.rs new file mode 100644 index 000000000..3632ee2cd --- /dev/null +++ b/mutiny-core/src/key.rs @@ -0,0 +1,70 @@ +use bitcoin::{ + secp256k1::Secp256k1, + util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}, +}; + +use crate::error::MutinyError; + +pub(crate) enum ChildKey { + NodeChildKey, +} + +impl ChildKey { + pub(crate) fn to_child_number(&self) -> u32 { + match self { + ChildKey::NodeChildKey => 0, + } + } +} + +pub(crate) fn create_root_child_key( + context: &Secp256k1, + xprivkey: ExtendedPrivKey, + child_key: ChildKey, +) -> Result { + let child_number = ChildNumber::from_hardened_idx(child_key.to_child_number())?; + + Ok(xprivkey.derive_priv(context, &DerivationPath::from(vec![child_number]))?) +} + +#[cfg(test)] +fn run_key_generation_tests() { + use bip39::Mnemonic; + use bitcoin::Network; + use std::str::FromStr; + + let context = Secp256k1::new(); + let mnemonic = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").expect("could not generate"); + let xpriv = ExtendedPrivKey::new_master(Network::Testnet, &mnemonic.to_seed("")).unwrap(); + + let first_root_key = create_root_child_key(&context, xpriv, ChildKey::NodeChildKey); + let copy_root_key = create_root_child_key(&context, xpriv, ChildKey::NodeChildKey); + + assert_eq!(first_root_key, copy_root_key); +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use crate::key::run_key_generation_tests; + + #[test] + fn key_generation_tests() { + run_key_generation_tests(); + } +} + +#[cfg(test)] +#[cfg(target_arch = "wasm32")] +mod wasm_tests { + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + use crate::key::run_key_generation_tests; + + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn key_generation_tests() { + run_key_generation_tests(); + } +} diff --git a/mutiny-core/src/keymanager.rs b/mutiny-core/src/keymanager.rs index ce16da5a8..812aaf5e2 100644 --- a/mutiny-core/src/keymanager.rs +++ b/mutiny-core/src/keymanager.rs @@ -1,8 +1,8 @@ -use crate::error::MutinyError; -use crate::labels::LabelStorage; use crate::logging::MutinyLogger; use crate::onchain::OnChainWallet; use crate::storage::MutinyStorage; +use crate::{error::MutinyError, key::create_root_child_key}; +use crate::{key::ChildKey, labels::LabelStorage}; use bdk::wallet::AddressIndex; use bip39::Mnemonic; use bitcoin::bech32::u5; @@ -223,10 +223,7 @@ pub(crate) fn create_keys_manager( ) -> Result, MutinyError> { let context = Secp256k1::new(); - let shared_key = xprivkey.derive_priv( - &context, - &DerivationPath::from(vec![ChildNumber::from_hardened_idx(0)?]), - )?; + let shared_key = create_root_child_key(&context, xprivkey, ChildKey::NodeChildKey)?; let xpriv = shared_key.derive_priv( &context, diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 04b21c794..771ecfe25 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -16,6 +16,7 @@ pub mod error; mod event; mod fees; mod gossip; +mod key; mod keymanager; pub mod labels; mod ldkstorage; @@ -44,8 +45,10 @@ 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::labels::{get_contact_key, Contact, LabelStorage}; use crate::logging::LOGGING_KEY; +use crate::nodemanager::{ + ChannelClosure, MutinyBip21RawMaterials, MutinyInvoice, TransactionDetails, +}; use crate::nostr::nwc::{ BudgetPeriod, BudgetedSpendingConditions, NwcProfileTag, SpendingConditions, }; @@ -53,24 +56,138 @@ use crate::nostr::MUTINY_PLUS_SUBSCRIPTION_LABEL; use crate::storage::{MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY}; use crate::{auth::MutinyAuthClient, logging::MutinyLogger}; use crate::{error::MutinyError, nostr::ReservedProfile}; +use crate::{ + labels::{get_contact_key, Contact, LabelStorage}, + nodemanager::NodeBalance, +}; use crate::{nodemanager::NodeManager, nostr::ProfileType}; use crate::{nostr::NostrManager, utils::sleep}; use ::nostr::key::XOnlyPublicKey; use ::nostr::{Event, Kind, Metadata}; +use bdk_chain::ConfirmationTime; use bip39::Mnemonic; -use bitcoin::secp256k1::PublicKey; use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; +use bitcoin::{hashes::sha256, secp256k1::PublicKey}; use futures::{pin_mut, select, FutureExt}; use lightning::{log_debug, util::logger::Logger}; use lightning::{log_error, log_info, log_warn}; use lightning_invoice::Bolt11Invoice; use nostr_sdk::{Client, RelayPoolNotification}; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::sync::atomic::Ordering; use std::sync::Arc; use std::{collections::HashMap, sync::atomic::AtomicBool}; +const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; + +#[derive(Copy, Clone)] +pub struct MutinyBalance { + pub confirmed: u64, + pub unconfirmed: u64, + pub lightning: u64, + pub force_close: u64, +} + +impl MutinyBalance { + fn new(ln_balance: NodeBalance) -> Self { + Self { + confirmed: ln_balance.confirmed, + unconfirmed: ln_balance.unconfirmed, + lightning: ln_balance.lightning, + force_close: ln_balance.force_close, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub enum ActivityItem { + OnChain(TransactionDetails), + Lightning(Box), + ChannelClosed(ChannelClosure), +} + +impl ActivityItem { + pub fn last_updated(&self) -> Option { + match self { + ActivityItem::OnChain(t) => match t.confirmation_time { + ConfirmationTime::Confirmed { time, .. } => Some(time), + ConfirmationTime::Unconfirmed { .. } => None, + }, + ActivityItem::Lightning(i) => match i.status { + HTLCStatus::Succeeded => Some(i.last_updated), + HTLCStatus::Failed => Some(i.last_updated), + HTLCStatus::Pending | HTLCStatus::InFlight => None, + }, + ActivityItem::ChannelClosed(c) => Some(c.timestamp), + } + } + + pub fn labels(&self) -> Vec { + match self { + ActivityItem::OnChain(t) => t.labels.clone(), + ActivityItem::Lightning(i) => i.labels.clone(), + ActivityItem::ChannelClosed(_) => vec![], + } + } + + pub fn is_channel_open(&self) -> bool { + match self { + ActivityItem::OnChain(onchain) => { + onchain.labels.iter().any(|l| l.contains("LN Channel:")) + } + ActivityItem::Lightning(_) => false, + ActivityItem::ChannelClosed(_) => false, + } + } +} + +impl PartialOrd for ActivityItem { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ActivityItem { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + // We want None to be greater than Some because those are pending transactions + // so those should be at the top of the list + let sort = match (self.last_updated(), other.last_updated()) { + (Some(self_time), Some(other_time)) => self_time.cmp(&other_time), + (Some(_), None) => core::cmp::Ordering::Less, + (None, Some(_)) => core::cmp::Ordering::Greater, + (None, None) => { + // if both are none, do lightning first + match (self, other) { + (ActivityItem::Lightning(_), ActivityItem::OnChain(_)) => { + core::cmp::Ordering::Greater + } + (ActivityItem::OnChain(_), ActivityItem::Lightning(_)) => { + core::cmp::Ordering::Less + } + (ActivityItem::Lightning(l1), ActivityItem::Lightning(l2)) => { + // compare lightning by expire time + l1.expire.cmp(&l2.expire) + } + (ActivityItem::OnChain(o1), ActivityItem::OnChain(o2)) => { + // compare onchain by confirmation time (which will be last seen for unconfirmed) + o1.confirmation_time.cmp(&o2.confirmation_time) + } + _ => core::cmp::Ordering::Equal, + } + } + }; + + // if the sort is equal, sort by serialization so we have a stable sort + sort.then_with(|| { + serde_json::to_string(self) + .unwrap() + .cmp(&serde_json::to_string(other).unwrap()) + }) + } +} + #[derive(Clone)] pub struct MutinyWalletConfig { xprivkey: ExtendedPrivKey, @@ -386,6 +503,106 @@ impl MutinyWallet { }); } + /// Pays a lightning invoice from the selected node. + /// An amount should only be provided if the invoice does not have an amount. + /// The amount should be in satoshis. + pub async fn pay_invoice( + &self, + inv: &Bolt11Invoice, + amt_sats: Option, + labels: Vec, + ) -> Result { + if inv.network() != self.config.network { + return Err(MutinyError::IncorrectNetwork(inv.network())); + } + + self.node_manager + .pay_invoice(None, inv, amt_sats, labels) + .await + } + + /// Creates a BIP 21 invoice. This creates a new address and a lightning invoice. + /// The lightning invoice may return errors related to the LSP. Check the error and + /// fallback to `get_new_address` and warn the user that Lightning is not available. + /// + /// Errors that might be returned include: + /// + /// - [`MutinyError::LspGenericError`]: This is returned for various reasons, including if a + /// request to the LSP server fails for any reason, or if the server returns + /// a status other than 500 that can't be parsed into a `ProposalResponse`. + /// + /// - [`MutinyError::LspFundingError`]: Returned if the LSP server returns an error with + /// a status of 500, indicating an "Internal Server Error", and a message + /// stating "Cannot fund new channel at this time". This means that the LSP cannot support + /// a new channel at this time. + /// + /// - [`MutinyError::LspAmountTooHighError`]: Returned if the LSP server returns an error with + /// a status of 500, indicating an "Internal Server Error", and a message stating "Invoice + /// amount is too high". This means that the LSP cannot support the amount that the user + /// requested. The user should request a smaller amount from the LSP. + /// + /// - [`MutinyError::LspConnectionError`]: Returned if the LSP server returns an error with + /// a status of 500, indicating an "Internal Server Error", and a message that starts with + /// "Failed to connect to peer". This means that the LSP is not connected to our node. + /// + /// If the server returns a status of 500 with a different error message, + /// a [`MutinyError::LspGenericError`] is returned. + pub async fn create_bip21( + &self, + amount: Option, + labels: Vec, + ) -> Result { + // If we are in safe mode, we don't create invoices + let invoice = if self.config.safe_mode { + None + } else { + let inv = self + .node_manager + .create_invoice(amount, labels.clone()) + .await?; + Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?) + }; + + let Ok(address) = self.node_manager.get_new_address(labels.clone()) else { + return Err(MutinyError::WalletOperationFailed); + }; + + Ok(MutinyBip21RawMaterials { + address, + invoice, + btc_amount: amount.map(|amount| bitcoin::Amount::from_sat(amount).to_btc().to_string()), + labels, + }) + } + + /// Gets the current balance of the wallet. + /// This includes both on-chain and lightning funds. + /// + /// This will not include any funds in an unconfirmed lightning channel. + pub async fn get_balance(&self) -> Result { + Ok(MutinyBalance::new(self.node_manager.get_balance().await?)) + } + + /// Get the sorted activity list for lightning payments, channels, and txs. + pub async fn get_activity(&self) -> Result, MutinyError> { + self.node_manager.get_activity().await + } + + /// Gets an invoice. + /// This includes sent and received invoices. + pub async fn get_invoice(&self, invoice: &Bolt11Invoice) -> Result { + self.get_invoice_by_hash(invoice.payment_hash()).await + } + + /// Looks up an invoice by hash. + /// This includes sent and received invoices. + pub async fn get_invoice_by_hash( + &self, + hash: &sha256::Hash, + ) -> Result { + self.node_manager.get_invoice_by_hash(hash).await + } + /// Checks whether or not the user is subscribed to Mutiny+. /// Submits a NWC string to keep the subscription active if not expired. /// @@ -417,18 +634,10 @@ impl MutinyWallet { autopay: bool, ) -> Result<(), MutinyError> { if let Some(subscription_client) = self.node_manager.subscription_client.clone() { - let nodes = self.node_manager.nodes.lock().await; - let first_node_pubkey = if let Some(node) = nodes.values().next() { - node.pubkey - } else { - return Err(MutinyError::WalletOperationFailed); - }; - drop(nodes); - // TODO if this times out, we should make the next part happen in EventManager self.node_manager .pay_invoice( - &first_node_pubkey, + None, inv, None, vec![MUTINY_PLUS_SUBSCRIPTION_LABEL.to_string()], @@ -661,6 +870,20 @@ impl MutinyWallet { storage.set_data(LOGGING_KEY.to_string(), logs, None)?; Ok(()) } + + /// Decodes a lightning invoice into useful information. + /// Will return an error if the invoice is for a different network. + pub fn decode_invoice( + &self, + invoice: Bolt11Invoice, + network: Option, + ) -> Result { + if invoice.network() != network.unwrap_or(self.config.network) { + return Err(MutinyError::IncorrectNetwork(invoice.network())); + } + + Ok(invoice.into()) + } } #[cfg(test)] @@ -874,4 +1097,46 @@ mod tests { let restored_seed = mw2.node_manager.xprivkey; assert_eq!(seed, restored_seed); } + + #[test] + async fn create_mutiny_wallet_safe_mode() { + let test_name = "create_mutiny_wallet"; + 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), None); + assert!(!NodeManager::has_node_manager(storage.clone())); + let config = MutinyWalletConfig::new( + xpriv, + #[cfg(target_arch = "wasm32")] + None, + Network::Regtest, + None, + None, + None, + None, + None, + None, + None, + None, + false, + true, + ) + .with_safe_mode(); + let mw = MutinyWallet::new(storage.clone(), config, None) + .await + .expect("mutiny wallet should initialize"); + mw.storage.insert_mnemonic(mnemonic).unwrap(); + assert!(NodeManager::has_node_manager(storage)); + + let bip21 = mw.create_bip21(None, vec![]).await.unwrap(); + assert!(bip21.invoice.is_none()); + + let new_node = mw.node_manager.new_node().await; + assert!(new_node.is_err()); + } } diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index f3f1611a4..71cf9ed88 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -1,4 +1,3 @@ -use crate::labels::LabelStorage; use crate::ldkstorage::{persist_monitor, ChannelOpenParams}; use crate::lsp::{InvoiceRequest, LspConfig}; use crate::messagehandler::MutinyMessageHandler; @@ -23,6 +22,7 @@ use crate::{ }; use crate::{fees::P2WSH_OUTPUT_SIZE, peermanager::connect_peer_if_necessary}; use crate::{keymanager::PhantomKeysManager, scorer::HubPreferentialScorer}; +use crate::{labels::LabelStorage, DEFAULT_PAYMENT_TIMEOUT}; use anyhow::{anyhow, Context}; use bdk::FeeRate; use bitcoin::hashes::{hex::ToHex, sha256::Hash as Sha256}; @@ -83,7 +83,6 @@ use std::{ }, }; -const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; const INITIAL_RECONNECTION_DELAY: u64 = 5; const MAX_RECONNECTION_DELAY: u64 = 60; @@ -1064,10 +1063,6 @@ impl Node { Ok(invoice) } - pub fn get_invoice(&self, invoice: &Bolt11Invoice) -> Result { - self.get_invoice_by_hash(invoice.payment_hash()) - } - pub fn get_invoice_by_hash(&self, payment_hash: &Sha256) -> Result { let (payment_info, inbound) = self.get_payment_info_from_persisters(payment_hash)?; let labels_map = self.persister.storage.get_invoice_labels()?; @@ -2294,7 +2289,7 @@ mod tests { _ => panic!("unexpected invoice description"), } - let from_storage = node.get_invoice(&invoice).unwrap(); + let from_storage = node.get_invoice_by_hash(invoice.payment_hash()).unwrap(); let by_hash = node.get_invoice_by_hash(invoice.payment_hash()).unwrap(); assert_eq!(from_storage, by_hash); @@ -2458,7 +2453,7 @@ mod wasm_test { _ => panic!("unexpected invoice description"), } - let from_storage = node.get_invoice(&invoice).unwrap(); + let from_storage = node.get_invoice_by_hash(invoice.payment_hash()).unwrap(); let by_hash = node.get_invoice_by_hash(invoice.payment_hash()).unwrap(); assert_eq!(from_storage, by_hash); diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 7cf1e492a..dcb1c456e 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -1,4 +1,3 @@ -use crate::lnurlauth::AuthManager; use crate::logging::LOGGING_KEY; use crate::redshift::{RedshiftManager, RedshiftStatus, RedshiftStorage}; use crate::storage::{MutinyStorage, DEVICE_ID_KEY, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY}; @@ -23,6 +22,7 @@ use crate::{ }; use crate::{gossip::*, scorer::HubPreferentialScorer}; use crate::{labels::LabelStorage, subscription::MutinySubscriptionClient}; +use crate::{lnurlauth::AuthManager, ActivityItem}; use anyhow::anyhow; use bdk::chain::{BlockId, ConfirmationTime}; use bdk::{wallet::AddressIndex, FeeRate, LocalUtxo}; @@ -379,94 +379,7 @@ impl Ord for ChannelClosure { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub enum ActivityItem { - OnChain(TransactionDetails), - Lightning(Box), - ChannelClosed(ChannelClosure), -} - -impl ActivityItem { - pub fn last_updated(&self) -> Option { - match self { - ActivityItem::OnChain(t) => match t.confirmation_time { - ConfirmationTime::Confirmed { time, .. } => Some(time), - ConfirmationTime::Unconfirmed { .. } => None, - }, - ActivityItem::Lightning(i) => match i.status { - HTLCStatus::Succeeded => Some(i.last_updated), - HTLCStatus::Failed => Some(i.last_updated), - HTLCStatus::Pending | HTLCStatus::InFlight => None, - }, - ActivityItem::ChannelClosed(c) => Some(c.timestamp), - } - } - - pub fn labels(&self) -> Vec { - match self { - ActivityItem::OnChain(t) => t.labels.clone(), - ActivityItem::Lightning(i) => i.labels.clone(), - ActivityItem::ChannelClosed(_) => vec![], - } - } - - pub fn is_channel_open(&self) -> bool { - match self { - ActivityItem::OnChain(onchain) => { - onchain.labels.iter().any(|l| l.contains("LN Channel:")) - } - ActivityItem::Lightning(_) => false, - ActivityItem::ChannelClosed(_) => false, - } - } -} - -impl PartialOrd for ActivityItem { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ActivityItem { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - // We want None to be greater than Some because those are pending transactions - // so those should be at the top of the list - let sort = match (self.last_updated(), other.last_updated()) { - (Some(self_time), Some(other_time)) => self_time.cmp(&other_time), - (Some(_), None) => core::cmp::Ordering::Less, - (None, Some(_)) => core::cmp::Ordering::Greater, - (None, None) => { - // if both are none, do lightning first - match (self, other) { - (ActivityItem::Lightning(_), ActivityItem::OnChain(_)) => { - core::cmp::Ordering::Greater - } - (ActivityItem::OnChain(_), ActivityItem::Lightning(_)) => { - core::cmp::Ordering::Less - } - (ActivityItem::Lightning(l1), ActivityItem::Lightning(l2)) => { - // compare lightning by expire time - l1.expire.cmp(&l2.expire) - } - (ActivityItem::OnChain(o1), ActivityItem::OnChain(o2)) => { - // compare onchain by confirmation time (which will be last seen for unconfirmed) - o1.confirmation_time.cmp(&o2.confirmation_time) - } - _ => core::cmp::Ordering::Equal, - } - } - }; - - // if the sort is equal, sort by serialization so we have a stable sort - sort.then_with(|| { - serde_json::to_string(self) - .unwrap() - .cmp(&serde_json::to_string(other).unwrap()) - }) - } -} - -pub struct MutinyBalance { +pub struct NodeBalance { pub confirmed: u64, pub unconfirmed: u64, pub lightning: u64, @@ -767,6 +680,19 @@ impl NodeManager { Ok(node.clone()) } + // New function to get a node by PublicKey or return the first node + pub(crate) async fn get_node_by_key_or_first( + &self, + pk: Option<&PublicKey>, + ) -> Result>, MutinyError> { + let nodes = self.nodes.lock().await; + let node = match pk { + Some(pubkey) => nodes.get(pubkey), + None => nodes.iter().next().map(|(_, node)| node), + }; + node.cloned().ok_or(MutinyError::NotFound) + } + /// Stops all of the nodes and background processes. /// Returns after node has been stopped. pub async fn stop(&self) -> Result<(), MutinyError> { @@ -960,57 +886,6 @@ impl NodeManager { Err(MutinyError::WalletOperationFailed) } - /// Creates a BIP 21 invoice. This creates a new address and a lightning invoice. - /// The lightning invoice may return errors related to the LSP. Check the error and - /// fallback to `get_new_address` and warn the user that Lightning is not available. - /// - /// Errors that might be returned include: - /// - /// - [`MutinyError::LspGenericError`]: This is returned for various reasons, including if a - /// request to the LSP server fails for any reason, or if the server returns - /// a status other than 500 that can't be parsed into a `ProposalResponse`. - /// - /// - [`MutinyError::LspFundingError`]: Returned if the LSP server returns an error with - /// a status of 500, indicating an "Internal Server Error", and a message - /// stating "Cannot fund new channel at this time". This means that the LSP cannot support - /// a new channel at this time. - /// - /// - [`MutinyError::LspAmountTooHighError`]: Returned if the LSP server returns an error with - /// a status of 500, indicating an "Internal Server Error", and a message stating "Invoice - /// amount is too high". This means that the LSP cannot support the amount that the user - /// requested. The user should request a smaller amount from the LSP. - /// - /// - [`MutinyError::LspConnectionError`]: Returned if the LSP server returns an error with - /// a status of 500, indicating an "Internal Server Error", and a message that starts with - /// "Failed to connect to peer". This means that the LSP is not connected to our node. - /// - /// If the server returns a status of 500 with a different error message, - /// a [`MutinyError::LspGenericError`] is returned. - pub async fn create_bip21( - &self, - amount: Option, - labels: Vec, - ) -> Result { - // If we are in safe mode, we don't create invoices - let invoice = if self.safe_mode { - None - } else { - let inv = self.create_invoice(amount, labels.clone()).await?; - Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?) - }; - - let Ok(address) = self.get_new_address(labels.clone()) else { - return Err(MutinyError::WalletOperationFailed); - }; - - Ok(MutinyBip21RawMaterials { - address, - invoice, - btc_amount: amount.map(|amount| bitcoin::Amount::from_sat(amount).to_btc().to_string()), - labels, - }) - } - pub async fn send_payjoin( &self, uri: PjUri<'_>, @@ -1250,7 +1125,7 @@ impl NodeManager { } /// Returns all the on-chain and lightning activity from the wallet. - pub async fn get_activity(&self) -> Result, MutinyError> { + pub(crate) async fn get_activity(&self) -> Result, MutinyError> { // todo add contacts to the activity let (lightning, closures) = futures_util::join!(self.list_invoices(), self.list_channel_closures()); @@ -1308,7 +1183,7 @@ impl NodeManager { let mut activity = vec![]; for inv in label_item.invoices.iter() { - let ln = self.get_invoice(inv).await?; + let ln = self.get_invoice_by_hash(inv.payment_hash()).await?; // Only show paid and in-flight invoices match ln.status { HTLCStatus::Succeeded | HTLCStatus::InFlight => { @@ -1398,7 +1273,7 @@ impl NodeManager { /// This includes both on-chain and lightning funds. /// /// This will not include any funds in an unconfirmed lightning channel. - pub async fn get_balance(&self) -> Result { + pub(crate) async fn get_balance(&self) -> Result { let onchain = if let Ok(wallet) = self.wallet.wallet.try_read() { wallet.get_balance() } else { @@ -1433,7 +1308,7 @@ impl NodeManager { .map(|bal| bal.claimable_amount_satoshis()) .sum(); - Ok(MutinyBalance { + Ok(NodeBalance { confirmed: onchain.confirmed + onchain.trusted_pending, unconfirmed: onchain.untrusted_pending + onchain.immature, lightning: lightning_msats / 1_000, @@ -1623,75 +1498,54 @@ impl NodeManager { Ok(peers) } - /// Attempts to connect to a peer from the selected node. + /// Attempts to connect to a peer using either a specified node or the first available node. pub async fn connect_to_peer( &self, - self_node_pubkey: &PublicKey, + self_node_pubkey: Option<&PublicKey>, connection_string: &str, label: Option, ) -> Result<(), MutinyError> { - if let Some(node) = self.nodes.lock().await.get(self_node_pubkey) { - let connect_info = PubkeyConnectionInfo::new(connection_string)?; - let label_opt = label.filter(|s| !s.is_empty()); // filter out empty strings - let res = node.connect_peer(connect_info, label_opt).await; - match res { - Ok(_) => { - log_info!(self.logger, "connected to peer: {connection_string}"); - return Ok(()); - } - Err(e) => { - log_error!( - self.logger, - "could not connect to peer: {connection_string} - {e}" - ); - return Err(e); - } - }; + let node = self.get_node_by_key_or_first(self_node_pubkey).await?; + let connect_info = PubkeyConnectionInfo::new(connection_string)?; + let label_opt = label.filter(|s| !s.is_empty()); // filter out empty strings + let res = node.connect_peer(connect_info, label_opt).await; + + match res { + Ok(_) => { + log_info!(self.logger, "Connected to peer: {connection_string}"); + Ok(()) + } + Err(e) => { + log_error!( + self.logger, + "Could not connect to peer: {connection_string} - {e}" + ); + Err(e) + } } - - log_error!( - self.logger, - "could not find internal node {self_node_pubkey}" - ); - Err(MutinyError::NotFound) } - /// Disconnects from a peer from the selected node. + /// Disconnects from a peer using either a specified node or the first available node. pub async fn disconnect_peer( &self, - self_node_pubkey: &PublicKey, + self_node_pubkey: Option<&PublicKey>, peer: PublicKey, ) -> Result<(), MutinyError> { - if let Some(node) = self.nodes.lock().await.get(self_node_pubkey) { - node.disconnect_peer(peer); - Ok(()) - } else { - log_error!( - self.logger, - "could not find internal node {self_node_pubkey}" - ); - Err(MutinyError::NotFound) - } + let node = self.get_node_by_key_or_first(self_node_pubkey).await?; + node.disconnect_peer(peer); + Ok(()) } - /// Deletes a peer from the selected node. - /// This will make it so that the node will not attempt to - /// reconnect to the peer. + /// Deletes a peer from either a specified node or the first available node. + /// This will prevent the node from attempting to reconnect to the peer. pub async fn delete_peer( &self, - self_node_pubkey: &PublicKey, + self_node_pubkey: Option<&PublicKey>, peer: &NodeId, ) -> Result<(), MutinyError> { - if let Some(node) = self.nodes.lock().await.get(self_node_pubkey) { - gossip::delete_peer_info(&self.storage, &node._uuid, peer)?; - Ok(()) - } else { - log_error!( - self.logger, - "could not find internal node {self_node_pubkey}" - ); - Err(MutinyError::NotFound) - } + let node = self.get_node_by_key_or_first(self_node_pubkey).await?; + gossip::delete_peer_info(&self.storage, &node._uuid, peer)?; + Ok(()) } /// Sets the label of a peer from the selected node. @@ -1742,55 +1596,37 @@ impl NodeManager { Ok(invoice.into()) } - /// Pays a lightning invoice from the selected node. + /// Pays a lightning invoice from either a specified node or the first available node. /// An amount should only be provided if the invoice does not have an amount. /// The amount should be in satoshis. - pub async fn pay_invoice( + pub(crate) async fn pay_invoice( &self, - from_node: &PublicKey, + self_node_pubkey: Option<&PublicKey>, invoice: &Bolt11Invoice, amt_sats: Option, labels: Vec, ) -> Result { - if invoice.network() != self.network { - return Err(MutinyError::IncorrectNetwork(invoice.network())); - } - - let node = self.get_node(from_node).await?; + let node = self.get_node_by_key_or_first(self_node_pubkey).await?; node.pay_invoice_with_timeout(invoice, amt_sats, None, labels) .await } - /// Sends a spontaneous payment to a node from the selected node. + /// Sends a spontaneous payment to a node from either a specified node or the first available node. /// The amount should be in satoshis. pub async fn keysend( &self, - from_node: &PublicKey, + self_node_pubkey: Option<&PublicKey>, to_node: PublicKey, amt_sats: u64, message: Option, labels: Vec, ) -> Result { - let node = self.get_node(from_node).await?; + let node = self.get_node_by_key_or_first(self_node_pubkey).await?; log_debug!(self.logger, "Keysending to {to_node}"); node.keysend_with_timeout(to_node, amt_sats, message, labels, None) .await } - /// Decodes a lightning invoice into useful information. - /// Will return an error if the invoice is for a different network. - pub async fn decode_invoice( - &self, - invoice: Bolt11Invoice, - network: Option, - ) -> Result { - if invoice.network() != network.unwrap_or(self.network) { - return Err(MutinyError::IncorrectNetwork(invoice.network())); - } - - Ok(invoice.into()) - } - /// Calls upon a LNURL to get the parameters for it. /// This contains what kind of LNURL it is (pay, withdrawal, auth, etc). // todo revamp LnUrlParams to be well designed @@ -1831,7 +1667,6 @@ impl NodeManager { /// This will fail if the LNURL is not a LNURL pay. pub async fn lnurl_pay( &self, - from_node: &PublicKey, lnurl: &LnUrl, amount_sats: u64, zap_npub: Option, @@ -1879,7 +1714,7 @@ impl NodeManager { } } - self.pay_invoice(from_node, &invoice, None, labels).await + self.pay_invoice(None, &invoice, None, labels).await } else { log_error!(self.logger, "LNURL return invoice with incorrect amount"); Err(MutinyError::LnUrlFailure) @@ -1934,19 +1769,7 @@ impl NodeManager { /// Gets an invoice from the node manager. /// This includes sent and received invoices. - pub async fn get_invoice(&self, invoice: &Bolt11Invoice) -> Result { - let nodes = self.nodes.lock().await; - let inv_opt: Option = - nodes.iter().find_map(|(_, n)| n.get_invoice(invoice).ok()); - match inv_opt { - Some(i) => Ok(i), - None => Err(MutinyError::NotFound), - } - } - - /// Gets an invoice from the node manager. - /// This includes sent and received invoices. - pub async fn get_invoice_by_hash( + pub(crate) async fn get_invoice_by_hash( &self, hash: &sha256::Hash, ) -> Result { @@ -1998,21 +1821,20 @@ impl NodeManager { Ok(channels) } - /// Opens a channel from our selected node to the given pubkey. + /// Opens a channel from either a specified node or the first available node to the given pubkey. /// The amount is in satoshis. /// /// The node must be online and have a connection to the peer. - /// The wallet much have enough funds to open the channel. + /// The wallet must have enough funds to open the channel. pub async fn open_channel( &self, - from_node: &PublicKey, + self_node_pubkey: Option<&PublicKey>, to_pubkey: Option, amount: u64, fee_rate: Option, user_channel_id: Option, ) -> Result { - let node = self.get_node(from_node).await?; - + let node = self.get_node_by_key_or_first(self_node_pubkey).await?; let to_pubkey = match to_pubkey { Some(pubkey) => pubkey, None => node @@ -2033,11 +1855,11 @@ impl NodeManager { match found_channel { Some(channel) => Ok(channel.into()), - None => Err(MutinyError::ChannelCreationFailed), // what should we do here? + None => Err(MutinyError::ChannelCreationFailed), } } - /// Opens a channel from our selected node to the given pubkey. + /// Opens a channel from either a specified node or the first available node to the given pubkey. /// It will spend the given utxos in full to fund the channel. /// /// The node must be online and have a connection to the peer. @@ -2045,12 +1867,11 @@ impl NodeManager { pub async fn sweep_utxos_to_channel( &self, user_chan_id: Option, - from_node: &PublicKey, + self_node_pubkey: Option<&PublicKey>, utxos: &[OutPoint], to_pubkey: Option, ) -> Result { - let node = self.get_node(from_node).await?; - + let node = self.get_node_by_key_or_first(self_node_pubkey).await?; let to_pubkey = match to_pubkey { Some(pubkey) => pubkey, None => node @@ -2071,7 +1892,7 @@ impl NodeManager { match found_channel { Some(channel) => Ok(channel.into()), - None => Err(MutinyError::ChannelCreationFailed), // what should we do here? + None => Err(MutinyError::ChannelCreationFailed), } } @@ -2082,7 +1903,7 @@ impl NodeManager { pub async fn sweep_all_to_channel( &self, user_chan_id: Option, - from_node: &PublicKey, + from_node: Option<&PublicKey>, to_pubkey: Option, ) -> Result { let utxos = self @@ -2723,50 +2544,6 @@ mod tests { } } - #[test] - async fn safe_mode_tests() { - let test_name = "safe_mode_tests"; - log!("{}", test_name); - - 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); - 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( - xpriv, - #[cfg(target_arch = "wasm32")] - None, - Network::Regtest, - None, - None, - None, - None, - None, - None, - None, - None, - false, - true, - ); - let c = c.with_safe_mode(); - - let nm = NodeManager::new( - c, - storage, - Arc::new(AtomicBool::new(false)), - Arc::new(MutinyLogger::default()), - ) - .await - .expect("node manager should initialize"); - - let bip21 = nm.create_bip21(None, vec![]).await.unwrap(); - assert!(bip21.invoice.is_none()); - - let new_node = nm.new_node().await; - assert!(new_node.is_err()); - } - #[test] async fn created_label_transaction() { let test_name = "created_new_nodes"; diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index ad22a5907..79860a283 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -864,7 +864,10 @@ impl NostrManager { // check if the invoice has been paid, if so, return, otherwise continue // checking for response event - if let Ok(invoice) = node_manager.get_invoice(&bolt11).await { + if let Ok(invoice) = node_manager + .get_invoice_by_hash(bolt11.payment_hash()) + .await + { if invoice.paid() { break; } diff --git a/mutiny-core/src/redshift.rs b/mutiny-core/src/redshift.rs index 0345e20dd..c84055880 100644 --- a/mutiny-core/src/redshift.rs +++ b/mutiny-core/src/redshift.rs @@ -175,7 +175,7 @@ impl RedshiftManager for NodeManager { let introduction_node = match (introduction_node, connection_string) { (Some(i), Some(c)) => { let connect_string = format!("{i}@{c}"); - self.connect_to_peer(&node.pubkey, &connect_string, None) + self.connect_to_peer(Some(&node.pubkey), &connect_string, None) .await?; i } @@ -215,7 +215,7 @@ impl RedshiftManager for NodeManager { let channel = self .sweep_utxos_to_channel( Some(user_chan_id), - &node.pubkey, + Some(&node.pubkey), &[utxo], Some(introduction_node), ) diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 9cd3e1a33..e43d3d16c 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -434,12 +434,7 @@ impl MutinyWallet { amount: Option, labels: Vec, ) -> Result { - Ok(self - .inner - .node_manager - .create_bip21(amount, labels) - .await? - .into()) + Ok(self.inner.create_bip21(amount, labels).await?.into()) } /// Sends an on-chain transaction to the given address. @@ -603,7 +598,7 @@ impl MutinyWallet { /// This will not include any funds in an unconfirmed lightning channel. #[wasm_bindgen] pub async fn get_balance(&self) -> Result { - Ok(self.inner.node_manager.get_balance().await?.into()) + Ok(self.inner.get_balance().await?.into()) } /// Lists all the UTXOs in the wallet. @@ -644,50 +639,30 @@ impl MutinyWallet { #[wasm_bindgen] pub async fn connect_to_peer( &self, - self_node_pubkey: String, connection_string: String, label: Option, ) -> Result<(), MutinyJsError> { - let self_node_pubkey = PublicKey::from_str(&self_node_pubkey)?; Ok(self .inner .node_manager - .connect_to_peer(&self_node_pubkey, &connection_string, label) + .connect_to_peer(None, &connection_string, label) .await?) } /// Disconnects from a peer from the selected node. #[wasm_bindgen] - pub async fn disconnect_peer( - &self, - self_node_pubkey: String, - peer: String, - ) -> Result<(), MutinyJsError> { - let self_node_pubkey = PublicKey::from_str(&self_node_pubkey)?; + pub async fn disconnect_peer(&self, peer: String) -> Result<(), MutinyJsError> { let peer = PublicKey::from_str(&peer)?; - Ok(self - .inner - .node_manager - .disconnect_peer(&self_node_pubkey, peer) - .await?) + Ok(self.inner.node_manager.disconnect_peer(None, peer).await?) } /// Deletes a peer from the selected node. /// This will make it so that the node will not attempt to /// reconnect to the peer. #[wasm_bindgen] - pub async fn delete_peer( - &self, - self_node_pubkey: String, - peer: String, - ) -> Result<(), MutinyJsError> { - let self_node_pubkey = PublicKey::from_str(&self_node_pubkey)?; + pub async fn delete_peer(&self, peer: String) -> Result<(), MutinyJsError> { let peer = NodeId::from_str(&peer)?; - Ok(self - .inner - .node_manager - .delete_peer(&self_node_pubkey, &peer) - .await?) + Ok(self.inner.node_manager.delete_peer(None, &peer).await?) } /// Sets the label of a peer from the selected node. @@ -724,17 +699,14 @@ impl MutinyWallet { #[wasm_bindgen] pub async fn pay_invoice( &self, - from_node: String, invoice_str: String, amt_sats: Option, labels: Vec, ) -> Result { - let from_node = PublicKey::from_str(&from_node)?; let invoice = Bolt11Invoice::from_str(&invoice_str)?; Ok(self .inner - .node_manager - .pay_invoice(&from_node, &invoice, amt_sats, labels) + .pay_invoice(&invoice, amt_sats, labels) .await? .into()) } @@ -744,18 +716,16 @@ impl MutinyWallet { #[wasm_bindgen] pub async fn keysend( &self, - from_node: String, to_node: String, amt_sats: u64, message: Option, labels: Vec, ) -> Result { - let from_node = PublicKey::from_str(&from_node)?; let to_node = PublicKey::from_str(&to_node)?; Ok(self .inner .node_manager - .keysend(&from_node, to_node, amt_sats, message, labels) + .keysend(None, to_node, amt_sats, message, labels) .await? .into()) } @@ -772,12 +742,7 @@ impl MutinyWallet { let network = network .map(|n| Network::from_str(&n).map_err(|_| MutinyJsError::InvalidArgumentsError)) .transpose()?; - Ok(self - .inner - .node_manager - .decode_invoice(invoice, network) - .await? - .into()) + Ok(self.inner.decode_invoice(invoice, network)?.into()) } /// Calls upon a LNURL to get the parameters for it. @@ -793,13 +758,11 @@ impl MutinyWallet { #[wasm_bindgen] pub async fn lnurl_pay( &self, - from_node: String, lnurl: String, amount_sats: u64, zap_npub: Option, labels: Vec, ) -> Result { - let from_node = PublicKey::from_str(&from_node)?; let lnurl = LnUrl::from_str(&lnurl)?; let zap_npub = match zap_npub.filter(|z| !z.is_empty()) { @@ -812,7 +775,7 @@ impl MutinyWallet { Ok(self .inner .node_manager - .lnurl_pay(&from_node, &lnurl, amount_sats, zap_npub, labels) + .lnurl_pay(&lnurl, amount_sats, zap_npub, labels) .await? .into()) } @@ -845,7 +808,7 @@ impl MutinyWallet { #[wasm_bindgen] pub async fn get_invoice(&self, invoice: String) -> Result { let invoice = Bolt11Invoice::from_str(&invoice)?; - Ok(self.inner.node_manager.get_invoice(&invoice).await?.into()) + Ok(self.inner.get_invoice(&invoice).await?.into()) } /// Gets an invoice from the node manager. @@ -853,12 +816,7 @@ impl MutinyWallet { #[wasm_bindgen] pub async fn get_invoice_by_hash(&self, hash: String) -> Result { let hash: sha256::Hash = sha256::Hash::from_str(&hash)?; - Ok(self - .inner - .node_manager - .get_invoice_by_hash(&hash) - .await? - .into()) + Ok(self.inner.get_invoice_by_hash(&hash).await?.into()) } /// Gets an invoice from the node manager. @@ -905,13 +863,10 @@ impl MutinyWallet { #[wasm_bindgen] pub async fn open_channel( &self, - from_node: String, to_pubkey: Option, amount: u64, fee_rate: Option, ) -> Result { - let from_node = PublicKey::from_str(&from_node)?; - let to_pubkey = match to_pubkey { Some(pubkey_str) if !pubkey_str.trim().is_empty() => { Some(PublicKey::from_str(&pubkey_str)?) @@ -922,7 +877,7 @@ impl MutinyWallet { Ok(self .inner .node_manager - .open_channel(&from_node, to_pubkey, amount, fee_rate, None) + .open_channel(None, to_pubkey, amount, fee_rate, None) .await? .into()) } @@ -933,11 +888,8 @@ impl MutinyWallet { /// The node must be online and have a connection to the peer. pub async fn sweep_all_to_channel( &self, - from_node: String, to_pubkey: Option, ) -> Result { - let from_node = PublicKey::from_str(&from_node)?; - let to_pubkey = match to_pubkey { Some(pubkey_str) if !pubkey_str.trim().is_empty() => { Some(PublicKey::from_str(&pubkey_str)?) @@ -948,7 +900,7 @@ impl MutinyWallet { Ok(self .inner .node_manager - .sweep_all_to_channel(None, &from_node, to_pubkey) + .sweep_all_to_channel(None, None, to_pubkey) .await? .into()) } @@ -998,7 +950,7 @@ impl MutinyWallet { #[wasm_bindgen] pub async fn get_activity(&self) -> Result */, MutinyJsError> { // get activity from the node manager - let activity = self.inner.node_manager.get_activity().await?; + let activity = self.inner.get_activity().await?; let mut activity: Vec = activity.into_iter().map(|a| a.into()).collect(); // add contacts to the activity diff --git a/mutiny-wasm/src/models.rs b/mutiny-wasm/src/models.rs index 509a86db6..54334a799 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -59,30 +59,30 @@ impl ActivityItem { } } -impl From for ActivityItem { - fn from(a: nodemanager::ActivityItem) -> Self { +impl From for ActivityItem { + fn from(a: mutiny_core::ActivityItem) -> Self { let kind = match a { - nodemanager::ActivityItem::OnChain(_) => { + mutiny_core::ActivityItem::OnChain(_) => { if a.is_channel_open() { ActivityType::ChannelOpen } else { ActivityType::OnChain } } - nodemanager::ActivityItem::Lightning(_) => ActivityType::Lightning, - nodemanager::ActivityItem::ChannelClosed(_) => ActivityType::ChannelClose, + mutiny_core::ActivityItem::Lightning(_) => ActivityType::Lightning, + mutiny_core::ActivityItem::ChannelClosed(_) => ActivityType::ChannelClose, }; let id = match a { - nodemanager::ActivityItem::OnChain(ref t) => t.txid.to_hex(), - nodemanager::ActivityItem::Lightning(ref ln) => ln.payment_hash.to_hex(), - nodemanager::ActivityItem::ChannelClosed(ref c) => { + mutiny_core::ActivityItem::OnChain(ref t) => t.txid.to_hex(), + mutiny_core::ActivityItem::Lightning(ref ln) => ln.payment_hash.to_hex(), + mutiny_core::ActivityItem::ChannelClosed(ref c) => { c.user_channel_id.map(|c| c.to_hex()).unwrap_or_default() } }; let (inbound, amount_sats) = match a { - nodemanager::ActivityItem::OnChain(ref t) => { + mutiny_core::ActivityItem::OnChain(ref t) => { let inbound = t.received > t.sent; let amount_sats = if inbound { Some(t.received - t.sent) @@ -91,8 +91,8 @@ impl From for ActivityItem { }; (inbound, amount_sats) } - nodemanager::ActivityItem::Lightning(ref ln) => (ln.inbound, ln.amount_sats), - nodemanager::ActivityItem::ChannelClosed(_) => (false, None), + mutiny_core::ActivityItem::Lightning(ref ln) => (ln.inbound, ln.amount_sats), + mutiny_core::ActivityItem::ChannelClosed(_) => (false, None), }; ActivityItem { @@ -414,8 +414,8 @@ impl MutinyBalance { } } -impl From for MutinyBalance { - fn from(m: nodemanager::MutinyBalance) -> Self { +impl From for MutinyBalance { + fn from(m: mutiny_core::MutinyBalance) -> Self { MutinyBalance { confirmed: m.confirmed, unconfirmed: m.unconfirmed, From 0d81bad1f87e52f99571a297c53db8fc4769f1df Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Sun, 19 Nov 2023 13:51:06 -0600 Subject: [PATCH 02/12] Add fedimint libraries --- .cargo/config | 2 + Cargo.lock | 1442 +++++++++++++++++++++++++++++++++++++++- mutiny-core/Cargo.toml | 8 + mutiny-wasm/Cargo.toml | 1 + 4 files changed, 1420 insertions(+), 33 deletions(-) diff --git a/.cargo/config b/.cargo/config index f4e8c002f..caaeecc69 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,4 @@ [build] target = "wasm32-unknown-unknown" +# for fedimint to use tokio::task::Builder - https://github.com/fedimint/fedimint/issues/3951 +rustflags = ["--cfg", "tokio_unstable"] diff --git a/Cargo.lock b/Cargo.lock index c92be13bc..1793e3be0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" + [[package]] name = "aho-corasick" version = "1.1.2" @@ -82,6 +88,20 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "aquamarine" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df752953c49ce90719c7bf1fc587bc8227aed04732ea0c0f85e5397d7fdbd1a1" +dependencies = [ + "include_dir", + "itertools 0.10.5", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "argon2" version = "0.5.2" @@ -94,6 +114,54 @@ dependencies = [ "password-hash 0.5.0", ] +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "async-trait" version = "0.1.74" @@ -155,12 +223,27 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64" version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64-compat" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" +dependencies = [ + "byteorder", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -224,6 +307,24 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip21" version = "0.3.1" @@ -241,6 +342,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" dependencies = [ "bitcoin_hashes 0.11.0", + "rand", + "rand_core", "serde", "unicode-normalization", ] @@ -254,6 +357,8 @@ dependencies = [ "base64 0.13.1", "bech32", "bitcoin_hashes 0.11.0", + "core2", + "hashbrown 0.8.2", "secp256k1 0.24.3", "serde", ] @@ -285,6 +390,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" dependencies = [ + "core2", "serde", ] @@ -298,6 +404,30 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoincore-rpc" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0261b2bb7617e0c91b452a837bbd1291fd34ad6990cb8e3ffc28239cc045b5ca" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c231bea28e314879c5aef240f6052e8a72a369e3c9f9b20d9bfbb33ad18029b2" +dependencies = [ + "bitcoin 0.29.2", + "serde", + "serde_json", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -310,13 +440,34 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", ] [[package]] @@ -337,12 +488,32 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bls12_381" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c196a77437e7cc2fb515ce413a6401291578b5afc8ecb29a3c7ab957f05941" +dependencies = [ + "ff", + "group", + "pairing", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "bumpalo" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + [[package]] name = "byteorder" version = "1.5.0" @@ -468,6 +639,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.11" @@ -477,6 +657,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -509,13 +695,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "crypto-common", "subtle", ] @@ -550,6 +745,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.6" @@ -572,12 +782,417 @@ dependencies = [ "serde", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fedimint-aead" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "argon2", + "hex", + "rand", + "ring 0.17.5", +] + +[[package]] +name = "fedimint-bip39" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "bip39", + "fedimint-client", + "fedimint-core", + "rand", +] + +[[package]] +name = "fedimint-bitcoind" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "async-trait", + "bitcoin 0.29.2", + "bitcoin_hashes 0.11.0", + "bitcoincore-rpc", + "esplora-client", + "fedimint-core", + "fedimint-logging", + "lazy_static", + "rand", + "serde", + "tracing", + "url", +] + +[[package]] +name = "fedimint-build" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "serde_json", +] + +[[package]] +name = "fedimint-client" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "aquamarine", + "async-stream", + "async-trait", + "bitcoin 0.29.2", + "bitcoin_hashes 0.11.0", + "fedimint-aead", + "fedimint-build", + "fedimint-core", + "fedimint-derive-secret", + "fedimint-logging", + "futures", + "itertools 0.10.5", + "rand", + "ring 0.17.5", + "secp256k1-zkp", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "fedimint-core" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "async-lock", + "async-recursion", + "async-trait", + "backtrace", + "bech32", + "bincode", + "bitcoin 0.29.2", + "bitcoin 0.30.1", + "bitcoin_hashes 0.11.0", + "bitvec", + "erased-serde", + "fedimint-derive", + "fedimint-logging", + "fedimint-tbs", + "fedimint-threshold-crypto", + "futures", + "getrandom", + "gloo-timers", + "hex", + "itertools 0.10.5", + "js-sys", + "jsonrpsee-core", + "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", + "lightning", + "lightning-invoice", + "macro_rules_attribute", + "miniscript", + "parity-scale-codec", + "rand", + "secp256k1-zkp", + "serde", + "serde_json", + "sha3", + "strum", + "strum_macros", + "thiserror", + "tokio", + "tokio-rustls 0.23.4", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "fedimint-derive" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fedimint-derive-secret" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "fedimint-core", + "fedimint-hkdf", + "fedimint-tbs", + "ring 0.17.5", + "secp256k1-zkp", +] + +[[package]] +name = "fedimint-hkdf" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "bitcoin_hashes 0.11.0", +] + +[[package]] +name = "fedimint-ln-client" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "aquamarine", + "async-stream", + "async-trait", + "bincode", + "bitcoin 0.29.2", + "bitcoin_hashes 0.11.0", + "erased-serde", + "fedimint-client", + "fedimint-core", + "fedimint-ln-common", + "fedimint-threshold-crypto", + "futures", + "itertools 0.10.5", + "lightning-invoice", + "rand", + "reqwest", + "secp256k1 0.24.3", + "secp256k1-zkp", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "fedimint-ln-common" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "aquamarine", + "async-trait", + "bitcoin 0.29.2", + "bitcoin_hashes 0.11.0", + "erased-serde", + "fedimint-client", + "fedimint-core", + "fedimint-threshold-crypto", + "futures", + "itertools 0.10.5", + "lightning", + "lightning-invoice", + "rand", + "secp256k1 0.24.3", + "serde", + "serde-big-array", + "serde_json", + "strum", + "strum_macros", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "fedimint-logging" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "tracing-subscriber", +] + +[[package]] +name = "fedimint-mint-client" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "aquamarine", + "async-stream", + "async-trait", + "base64 0.20.0", + "bincode", + "bitcoin_hashes 0.11.0", + "erased-serde", + "fedimint-client", + "fedimint-core", + "fedimint-derive-secret", + "fedimint-logging", + "fedimint-mint-common", + "fedimint-tbs", + "fedimint-threshold-crypto", + "futures", + "itertools 0.10.5", + "rand", + "secp256k1 0.24.3", + "secp256k1-zkp", + "serde", + "serde-big-array", + "serde_json", + "strum", + "strum_macros", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "fedimint-mint-common" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bitcoin_hashes 0.11.0", + "fedimint-core", + "fedimint-tbs", + "fedimint-threshold-crypto", + "futures", + "itertools 0.10.5", + "rand", + "secp256k1 0.24.3", + "secp256k1-zkp", + "serde", + "strum", + "strum_macros", + "thiserror", + "tracing", +] + +[[package]] +name = "fedimint-tbs" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "bitcoin_hashes 0.11.0", + "bls12_381", + "ff", + "group", + "rand", + "rand_chacha", + "serde", + "sha3", +] + +[[package]] +name = "fedimint-threshold-crypto" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2930eda59c029045497a7ef03799d01eaba6091a03713bef5366bd79aab423" +dependencies = [ + "bls12_381", + "byteorder", + "ff", + "group", + "hex_fmt", + "log", + "pairing", + "rand", + "rand_chacha", + "serde", + "subtle", + "thiserror", + "tiny-keccak", + "zeroize", +] + +[[package]] +name = "fedimint-wallet-client" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "aquamarine", + "async-stream", + "async-trait", + "bitcoin 0.29.2", + "erased-serde", + "fedimint-bitcoind", + "fedimint-client", + "fedimint-core", + "fedimint-wallet-common", + "futures", + "impl-tools", + "miniscript", + "rand", + "secp256k1 0.24.3", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", + "tokio", + "tracing", + "url", + "validator", +] + +[[package]] +name = "fedimint-wallet-common" +version = "0.2.1-rc2" +source = "git+https://github.com/fedimint/fedimint.git?rev=51d09c76b8a15615114d4f2c2663c738d8ff3135#51d09c76b8a15615114d4f2c2663c738d8ff3135" +dependencies = [ + "anyhow", + "async-trait", + "bitcoin 0.29.2", + "erased-serde", + "fedimint-core", + "futures", + "impl-tools", + "miniscript", + "rand", + "secp256k1 0.24.3", + "serde", + "strum", + "strum_macros", + "thiserror", + "tracing", + "url", + "validator", +] + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -623,6 +1238,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.29" @@ -694,6 +1315,16 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] + [[package]] name = "futures-util" version = "0.3.29" @@ -797,6 +1428,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.21" @@ -809,7 +1451,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -822,18 +1464,55 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "hashbrown" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" +dependencies = [ + "ahash", + "autocfg", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + [[package]] name = "hex_lit" version = "0.1.1" @@ -846,7 +1525,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -916,9 +1595,9 @@ dependencies = [ "futures-util", "http", "hyper", - "rustls", + "rustls 0.21.8", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", ] [[package]] @@ -967,6 +1646,66 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "impl-tools" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bde75c0fa563af28932bd7317eaa58186d4d29043858f5f3a8c199076c06da7" +dependencies = [ + "autocfg", + "impl-tools-lib", + "proc-macro-error", + "syn 1.0.109", +] + +[[package]] +name = "impl-tools-lib" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffdf3b0bdfaa18798f4df71bc965704f63fba521b24d425cd61fb2bc771a77b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "include_dir" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -974,7 +1713,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", ] [[package]] @@ -1038,6 +1787,100 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpc" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8423b78fc94d12ef1a4a9d13c348c9a78766dda0cc18817adf0faf77e670c8" +dependencies = [ + "base64-compat", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b005c793122d03217da09af68ba9383363caa950b90d3436106df8cabce935" +dependencies = [ + "futures-channel", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core", + "pin-project", + "soketto", + "thiserror", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "tracing", + "url", + "webpki-roots", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2327ba8df2fdbd5e897e2b5ed25ce7f299d345b9736b6828814c3dbd1fd47b" +dependencies = [ + "anyhow", + "async-lock", + "async-trait", + "beef", + "futures-timer", + "futures-util", + "jsonrpsee-types", + "rustc-hash", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be0be325642e850ed0bdff426674d2e66b2b7117c9be23a7caef68a2902b7d9" +dependencies = [ + "anyhow", + "beef", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7cbb3447cf14fd4d2f407c3cc96e6c9634d5440aa1fbed868a31f3c02b27f0" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca9cb3933ccae417eb6b08c3448eb1cb46e39834e5b503e395e5e5bd08546c0" +dependencies = [ + "http", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "url", +] + [[package]] name = "jwt-compact" version = "0.8.0" @@ -1060,6 +1903,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1078,6 +1930,8 @@ version = "0.0.118" source = "git+https://github.com/MutinyWallet/rust-lightning.git?rev=686a84236f54bf8d7270a5fbec07801e5281691f#686a84236f54bf8d7270a5fbec07801e5281691f" dependencies = [ "bitcoin 0.29.2", + "core2", + "hashbrown 0.8.2", "musig2", ] @@ -1166,12 +2020,47 @@ dependencies = [ "url", ] +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +[[package]] +name = "macro_rules_attribute" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.6.4" @@ -1269,6 +2158,13 @@ dependencies = [ "cfg-if", "chrono", "esplora-client", + "fedimint-bip39", + "fedimint-client", + "fedimint-core", + "fedimint-ln-client", + "fedimint-ln-common", + "fedimint-mint-client", + "fedimint-wallet-client", "futures", "futures-util", "getrandom", @@ -1313,6 +2209,7 @@ dependencies = [ "bip39", "bitcoin 0.29.2", "console_error_panic_hook", + "fedimint-core", "futures", "getrandom", "gloo-utils", @@ -1407,7 +2304,7 @@ dependencies = [ "futures-util", "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-socks", "tokio-tungstenite 0.20.1", "url-fork", @@ -1415,6 +2312,16 @@ dependencies = [ "ws_stream_wasm", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -1499,6 +2406,70 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pairing" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b" +dependencies = [ + "group", +] + +[[package]] +name = "parity-scale-codec" +version = "3.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dec8a8073036902368c2cdc0387e85ff9a37054d7e7c98e592145e0c92cd4fb" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312270ee71e1cd70289dacf597cab7b207aa107d2f28191c2ae45b2ece18a260" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -1521,6 +2492,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "payjoin" version = "0.10.0" @@ -1539,7 +2516,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest", + "digest 0.10.7", "hmac", "password-hash 0.4.2", "sha2", @@ -1644,13 +2621,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] -name = "predicates-tree" -version = "1.0.9" +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "predicates-core", - "termtree", + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -1671,6 +2682,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1718,8 +2735,17 @@ checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1730,9 +2756,15 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.2" @@ -1764,7 +2796,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.21.8", "rustls-pemfile", "serde", "serde_json", @@ -1772,7 +2804,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-socks", "tower-service", "url", @@ -1798,6 +2830,21 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.5" @@ -1807,8 +2854,8 @@ dependencies = [ "cc", "getrandom", "libc", - "spin", - "untrusted", + "spin 0.9.8", + "untrusted 0.9.0", "windows-sys", ] @@ -1818,6 +2865,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1840,6 +2893,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + [[package]] name = "rustls" version = "0.21.8" @@ -1847,7 +2912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", - "ring", + "ring 0.17.5", "rustls-webpki", "sct", ] @@ -1867,10 +2932,16 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.15" @@ -1892,14 +2963,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sct" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -1962,6 +3039,28 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-zkp" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd403e9f0569b4131ab3fc9fa24a17775331b39382efd2cde851fdca655e3520" +dependencies = [ + "rand", + "secp256k1 0.24.3", + "secp256k1-zkp-sys", + "serde", +] + +[[package]] +name = "secp256k1-zkp-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e7a2beac087c1da2d21018a3b7f043fe2f138654ad9c1518d409061a4a0034" +dependencies = [ + "cc", + "secp256k1-sys 0.6.1", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -1991,6 +3090,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + [[package]] name = "send_wrapper" version = "0.6.0" @@ -2006,6 +3111,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.192" @@ -2040,6 +3154,19 @@ dependencies = [ "serde", ] +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2048,7 +3175,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -2059,7 +3186,35 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", ] [[package]] @@ -2097,12 +3252,52 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "soketto" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1c5305e39e09653383c2c7244f2f78b3bcae37cf50c64cb4789c9f5096ec2" +dependencies = [ + "base64 0.13.1", + "bytes", + "futures", + "httparse", + "log", + "rand", + "sha-1", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "subtle" version = "2.5.0" @@ -2152,6 +3347,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.8.1" @@ -2191,6 +3392,25 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2217,9 +3437,12 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2 0.5.5", "tokio-macros", + "tracing", "windows-sys", ] @@ -2244,13 +3467,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + [[package]] name = "tokio-rustls" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.8", "tokio", ] @@ -2288,9 +3522,9 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", - "rustls", + "rustls 0.21.8", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tungstenite 0.20.1", "webpki-roots", ] @@ -2303,12 +3537,30 @@ checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", "tracing", ] +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -2322,9 +3574,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -2332,6 +3596,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2373,7 +3667,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls", + "rustls 0.21.8", "sha1", "thiserror", "url", @@ -2417,6 +3711,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -2469,6 +3769,54 @@ dependencies = [ "getrandom", ] +[[package]] +name = "validator" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2608,6 +3956,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.5", + "untrusted 0.9.0", +] + [[package]] name = "webpki-roots" version = "0.25.2" @@ -2711,6 +4069,15 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winnow" +version = "0.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -2733,13 +4100,22 @@ dependencies = [ "log", "pharos", "rustc_version", - "send_wrapper", + "send_wrapper 0.6.0", "thiserror", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index e4cfe2089..ee5f108cb 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -46,6 +46,14 @@ jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] } argon2 = { version = "0.5.0", features = ["password-hash", "alloc"] } payjoin = { version = "0.10.0", features = ["send", "base64"] } +fedimint-client = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } +fedimint-core = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } +fedimint-wallet-client = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } +fedimint-mint-client = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } +fedimint-ln-client = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } +fedimint-bip39 = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } +fedimint-ln-common = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } + base64 = "0.13.0" pbkdf2 = "0.11" aes-gcm = "0.10.1" diff --git a/mutiny-wasm/Cargo.toml b/mutiny-wasm/Cargo.toml index 995770d8e..57d7ef6b6 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -42,6 +42,7 @@ futures = "0.3.25" urlencoding = "2.1.2" once_cell = "1.18.0" payjoin = { version = "0.10.0", features = ["send", "base64"] } +fedimint-core = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires From dcd52f3e8df66b3e3f21d8c04d73b061a3985459 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Thu, 23 Nov 2023 15:15:21 -0600 Subject: [PATCH 03/12] Add, remove, and list federations --- mutiny-core/src/federation.rs | 242 ++++++++++++++++++++++++++++++++++ mutiny-core/src/key.rs | 6 +- mutiny-core/src/lib.rs | 171 ++++++++++++++++++++++++ mutiny-core/src/onchain.rs | 16 ++- mutiny-core/src/storage.rs | 21 ++- mutiny-wasm/src/lib.rs | 40 +++++- mutiny-wasm/src/models.rs | 36 +++++ 7 files changed, 523 insertions(+), 9 deletions(-) create mode 100644 mutiny-core/src/federation.rs diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs new file mode 100644 index 000000000..778fc7745 --- /dev/null +++ b/mutiny-core/src/federation.rs @@ -0,0 +1,242 @@ +use crate::{ + error::MutinyError, + key::{create_root_child_key, ChildKey}, + logging::MutinyLogger, + onchain::coin_type_from_network, + storage::MutinyStorage, +}; +use bip39::Mnemonic; +use bitcoin::{secp256k1::Secp256k1, util::bip32::ExtendedPrivKey}; +use bitcoin::{ + util::bip32::{ChildNumber, DerivationPath}, + Network, +}; +use fedimint_bip39::Bip39RootSecretStrategy; +use fedimint_client::{ + derivable_secret::DerivableSecret, + secret::{get_default_client_secret, RootSecretStrategy}, + ClientArc, FederationInfo, +}; +use fedimint_core::db::mem_impl::MemDatabase; +use fedimint_core::{api::InviteCode, config::FederationId}; +use fedimint_ln_client::LightningClientInit; +use fedimint_mint_client::MintClientInit; +use fedimint_wallet_client::WalletClientInit; +use lightning::log_info; +use lightning::util::logger::Logger; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::{atomic::AtomicBool, Arc, RwLock}, +}; + +// This is the FederationStorage object saved to the DB +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct FederationStorage { + pub federations: HashMap, + pub version: u32, +} + +// This is the FederationIdentity that refer to a specific federation +// Used for public facing identification. +pub struct FederationIdentity { + pub uuid: String, + pub federation_id: FederationId, +} + +// This is the FederationIndex reference that is saved to the DB +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct FederationIndex { + pub federation_code: InviteCode, +} + +// TODO remove +#[allow(dead_code)] +pub(crate) struct FederationClient { + pub(crate) _uuid: String, + pub(crate) federation_index: FederationIndex, + pub(crate) federation_code: InviteCode, + pub(crate) fedimint_client: ClientArc, + stopped_components: Arc>>, + storage: S, + network: Network, + pub(crate) logger: Arc, + stop: Arc, +} + +// TODO remove +#[allow(dead_code)] +impl FederationClient { + #[allow(clippy::too_many_arguments)] + pub(crate) async fn new( + uuid: String, + federation_index: &FederationIndex, + federation_code: InviteCode, + xprivkey: ExtendedPrivKey, + storage: S, + network: Network, + logger: Arc, + stop: Arc, + ) -> Result { + log_info!(logger, "initializing a new federation client: {uuid}"); + + // a list of components that need to be stopped and whether or not they are stopped + // TODO remove this if we end up not needing to stop things + let stopped_components = Arc::new(RwLock::new(vec![])); + + log_info!(logger, "Joining federation {}", federation_code); + + let federation_info = FederationInfo::from_invite_code(federation_code.clone()).await?; + + let mut client_builder = fedimint_client::Client::builder(); + client_builder.with_module(WalletClientInit(None)); + client_builder.with_module(MintClientInit); + client_builder.with_module(LightningClientInit); + client_builder.with_database(MemDatabase::new().into()); // TODO not in memory + client_builder.with_primary_module(1); + client_builder + .with_federation_info(FederationInfo::from_invite_code(federation_code.clone()).await?); + + let secret = create_federation_secret(xprivkey, network)?; + + let fedimint_client = client_builder + .build(get_default_client_secret( + &secret, + &federation_info.federation_id(), + )) + .await?; + + Ok(FederationClient { + _uuid: uuid, + federation_index: federation_index.clone(), + federation_code, + fedimint_client, + stopped_components, + storage, + network, + logger, + stop, + }) + } +} + +// A federation private key will be derived from +// `m/1'/N'` where `N` is the network type. +// +// Federation will derive further keys from there. +fn create_federation_secret( + xprivkey: ExtendedPrivKey, + network: Network, +) -> Result { + let context = Secp256k1::new(); + + let shared_key = create_root_child_key(&context, xprivkey, ChildKey::FederationChildKey)?; + let xpriv = shared_key.derive_priv( + &context, + &DerivationPath::from(vec![ChildNumber::from_hardened_idx( + coin_type_from_network(network), + )?]), + )?; + + // now that we have a private key for our federation secret, turn that into a mnemonic so we + // can derive it just like fedimint does in case we ever want to expose the mnemonic for + // fedimint cross compatibility. + let mnemonic = mnemonic_from_xpriv(xpriv)?; + Ok(Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic)) +} + +pub(crate) fn mnemonic_from_xpriv(xpriv: ExtendedPrivKey) -> Result { + let mnemonic = Mnemonic::from_entropy(&xpriv.private_key.secret_bytes())?; + Ok(mnemonic) +} + +#[cfg(test)] +fn fedimint_seed_generation() { + use crate::generate_seed; + + let mnemonic = generate_seed(12).unwrap(); + + let xpriv_regtest = + ExtendedPrivKey::new_master(Network::Regtest, &mnemonic.to_seed("")).unwrap(); + let fed_secret_regtest = create_federation_secret(xpriv_regtest, Network::Regtest).unwrap(); + + // create mainnet to ensure different + let xpriv_mainnet = + ExtendedPrivKey::new_master(Network::Bitcoin, &mnemonic.to_seed("")).unwrap(); + let fed_secret_mainnet = create_federation_secret(xpriv_mainnet, Network::Bitcoin).unwrap(); + + assert_ne!( + fed_secret_regtest.to_chacha20_poly1305_key_raw(), + fed_secret_mainnet.to_chacha20_poly1305_key_raw(), + ); +} + +#[cfg(test)] +fn fedimint_mnemonic_generation() { + use super::*; + use std::str::FromStr; + + let mnemonic_str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let root_mnemonic = Mnemonic::from_str(mnemonic_str).expect("could not generate"); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &root_mnemonic.to_seed("")).unwrap(); + let context = Secp256k1::new(); + let child_key = create_root_child_key(&context, xpriv, ChildKey::FederationChildKey).unwrap(); + + let child_mnemonic = mnemonic_from_xpriv(child_key).unwrap(); + assert_ne!(mnemonic_str, child_mnemonic.to_string()); + + let expected_child_mnemonic = "discover lift vanish gas also begin elevator must easily front kiwi motor glow shy lady sound crash flat bulk tilt sick super daring polar"; + assert_eq!(expected_child_mnemonic, child_mnemonic.to_string()); + + // Do it again with different mnemonic + let mnemonic_str2 = "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always"; + let root_mnemonic2 = Mnemonic::from_str(mnemonic_str2).expect("could not generate"); + let xpriv2 = + ExtendedPrivKey::new_master(Network::Regtest, &root_mnemonic2.to_seed("")).unwrap(); + let context2 = Secp256k1::new(); + let child_key2 = + create_root_child_key(&context2, xpriv2, ChildKey::FederationChildKey).unwrap(); + + let child_mnemonic2 = mnemonic_from_xpriv(child_key2).unwrap(); + assert_ne!(mnemonic_str2, child_mnemonic2.to_string()); + + let expected_child_mnemonic2 = "jewel primary rice smile garage lucky bullet scheme crack vehicle real urban pen another squeeze rate sorry never afraid chief proof decline reveal history"; + assert_ne!(expected_child_mnemonic, expected_child_mnemonic2); + assert_eq!(expected_child_mnemonic2, child_mnemonic2.to_string()); +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use super::*; + + #[test] + fn test_fedimint_seed_generation() { + fedimint_seed_generation(); + } + + #[test] + fn test_fedimint_mnemonic_generation() { + fedimint_mnemonic_generation(); + } +} + +#[cfg(test)] +#[cfg(target_arch = "wasm32")] +mod wasm_tests { + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + use super::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn test_fedimint_seed_generation() { + fedimint_seed_generation(); + } + + #[test] + fn test_fedimint_mnemonic_generation() { + fedimint_mnemonic_generation(); + } +} diff --git a/mutiny-core/src/key.rs b/mutiny-core/src/key.rs index 3632ee2cd..dc6d845e3 100644 --- a/mutiny-core/src/key.rs +++ b/mutiny-core/src/key.rs @@ -7,12 +7,14 @@ use crate::error::MutinyError; pub(crate) enum ChildKey { NodeChildKey, + FederationChildKey, } impl ChildKey { pub(crate) fn to_child_number(&self) -> u32 { match self { ChildKey::NodeChildKey => 0, + ChildKey::FederationChildKey => 1, } } } @@ -39,8 +41,10 @@ fn run_key_generation_tests() { let first_root_key = create_root_child_key(&context, xpriv, ChildKey::NodeChildKey); let copy_root_key = create_root_child_key(&context, xpriv, ChildKey::NodeChildKey); - assert_eq!(first_root_key, copy_root_key); + + let federation_root_key = create_root_child_key(&context, xpriv, ChildKey::FederationChildKey); + assert_ne!(first_root_key, federation_root_key); } #[cfg(test)] diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 771ecfe25..2b5887eca 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -14,6 +14,7 @@ mod chain; pub mod encrypt; pub mod error; mod event; +pub mod federation; mod fees; mod gossip; mod key; @@ -45,6 +46,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::federation::{FederationClient, FederationIdentity, FederationIndex, FederationStorage}; use crate::logging::LOGGING_KEY; use crate::nodemanager::{ ChannelClosure, MutinyBip21RawMaterials, MutinyInvoice, TransactionDetails, @@ -69,7 +71,9 @@ use bip39::Mnemonic; use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; use bitcoin::{hashes::sha256, secp256k1::PublicKey}; +use fedimint_core::{api::InviteCode, config::FederationId}; use futures::{pin_mut, select, FutureExt}; +use futures_util::lock::Mutex; use lightning::{log_debug, util::logger::Logger}; use lightning::{log_error, log_info, log_warn}; use lightning_invoice::Bolt11Invoice; @@ -79,6 +83,7 @@ use serde_json::{json, Value}; use std::sync::atomic::Ordering; use std::sync::Arc; use std::{collections::HashMap, sync::atomic::AtomicBool}; +use uuid::Uuid; const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; @@ -265,6 +270,8 @@ pub struct MutinyWallet { pub storage: S, pub node_manager: Arc>, pub nostr: Arc>, + pub federation_storage: Arc>, + pub(crate) federations: Arc>>>>, pub stop: Arc, pub logger: Arc, } @@ -310,6 +317,12 @@ impl MutinyWallet { node_manager.logger.clone(), )?); + // create federation library + let (federation_storage, federations) = + create_federations(&storage, &config, &logger, stop.clone()).await?; + let federation_storage = Arc::new(Mutex::new(federation_storage)); + let federations = federations; + if !config.skip_hodl_invoices { log_warn!( node_manager.logger, @@ -322,6 +335,8 @@ impl MutinyWallet { storage, node_manager, nostr, + federation_storage, + federations, stop, logger, }; @@ -884,6 +899,162 @@ impl MutinyWallet { Ok(invoice.into()) } + + /// Adds a new federation based on its federation code + pub async fn new_federation( + &self, + federation_code: InviteCode, + ) -> Result { + create_new_federation( + self.config.xprivkey, + self.storage.clone(), + self.config.network, + self.logger.clone(), + self.federation_storage.clone(), + self.federations.clone(), + federation_code, + self.stop.clone(), + ) + .await + } + + /// Lists the federation id's of the federation clients in the manager. + pub async fn list_federations(&self) -> Result, MutinyError> { + let federations = self.federations.lock().await; + let federation_ids = federations + .iter() + .map(|(_, n)| n.fedimint_client.federation_id()) + .collect(); + Ok(federation_ids) + } + + /// Removes a federation by setting its archived status to true, based on the FederationId. + pub async fn remove_federation(&self, federation_id: FederationId) -> Result<(), MutinyError> { + let mut federations_guard = self.federations.lock().await; + + if let Some(fedimint_client) = federations_guard.get(&federation_id) { + let uuid = &fedimint_client._uuid; + + let mut federation_storage_guard = self.federation_storage.lock().await; + + if federation_storage_guard.federations.contains_key(uuid) { + federation_storage_guard.federations.remove(uuid); + self.storage + .insert_federations(federation_storage_guard.clone())?; + federations_guard.remove(&federation_id); + } else { + return Err(MutinyError::NotFound); + } + } else { + return Err(MutinyError::NotFound); + } + + Ok(()) + } +} + +async fn create_federations( + storage: &S, + c: &MutinyWalletConfig, + logger: &Arc, + stop: Arc, +) -> Result< + ( + FederationStorage, + Arc>>>>, + ), + MutinyError, +> { + let federation_storage = storage.get_federations()?; + let federations = federation_storage.clone().federations.into_iter(); + let mut federation_map = HashMap::new(); + for federation_item in federations { + let federation = FederationClient::new( + federation_item.0, + &federation_item.1, + federation_item.1.federation_code.clone(), + c.xprivkey, + storage.clone(), + c.network, + logger.clone(), + stop.clone(), + ) + .await?; + + let id = federation.fedimint_client.federation_id(); + + federation_map.insert(id, Arc::new(federation)); + } + let federations = Arc::new(Mutex::new(federation_map)); + Ok((federation_storage, federations)) +} + +// This will create a new federation and returns the Federation ID of the client created. +#[allow(clippy::too_many_arguments)] +pub(crate) async fn create_new_federation( + xprivkey: ExtendedPrivKey, + storage: S, + network: Network, + logger: Arc, + federation_storage: Arc>, + federations: Arc>>>>, + federation_code: InviteCode, + stop: Arc, +) -> Result { + // Begin with a mutex lock so that nothing else can + // save or alter the federation list while it is about to + // be saved. + let mut federation_mutex = federation_storage.lock().await; + + // Get the current federations so that we can check if the new federation already exists + let mut existing_federations = storage.get_federations()?; + + // Check if the federation already exists + if existing_federations + .federations + .values() + .any(|federation| federation.federation_code == federation_code) + { + return Err(MutinyError::InvalidArgumentsError); + } + + // Create and save a new federation + let next_federation_uuid = Uuid::new_v4().to_string(); + let next_federation = FederationIndex { + federation_code: federation_code.clone(), + }; + + existing_federations.version += 1; + existing_federations + .federations + .insert(next_federation_uuid.clone(), next_federation.clone()); + + storage.insert_federations(existing_federations.clone())?; + federation_mutex.federations = existing_federations.federations.clone(); + + // now create the federation process and init it + let new_federation = FederationClient::new( + next_federation_uuid.clone(), + &next_federation, + federation_code, + xprivkey, + storage.clone(), + network, + logger.clone(), + stop, + ) + .await?; + + let federation_id = new_federation.fedimint_client.federation_id(); + federations + .lock() + .await + .insert(federation_id, Arc::new(new_federation)); + + Ok(FederationIdentity { + uuid: next_federation_uuid.clone(), + federation_id, + }) } #[cfg(test)] diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index c145678d8..15ab74bf5 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -625,12 +625,7 @@ fn get_tr_descriptors_for_extended_key( network: Network, account_number: u32, ) -> Result<(DescriptorTemplateOut, DescriptorTemplateOut), MutinyError> { - let coin_type = match network { - Network::Bitcoin => 0, - Network::Testnet => 1, - Network::Signet => 1, - Network::Regtest => 1, - }; + let coin_type = coin_type_from_network(network); let base_path = DerivationPath::from_str("m/86'")?; let derivation_path = base_path.extend([ @@ -650,6 +645,15 @@ fn get_tr_descriptors_for_extended_key( Ok((receive_descriptor_template, change_descriptor_template)) } +pub(crate) fn coin_type_from_network(network: Network) -> u32 { + match network { + Network::Bitcoin => 0, + Network::Testnet => 1, + Network::Signet => 1, + Network::Regtest => 1, + } +} + pub(crate) fn get_esplora_url(network: Network, user_provided_url: Option) -> String { if let Some(url) = user_provided_url { url diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 8ce3d93ee..2ce22e8a1 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -1,9 +1,12 @@ -use crate::encrypt::{decrypt_with_password, encrypt, encryption_key_from_pass, Cipher}; use crate::error::{MutinyError, MutinyStorageError}; use crate::ldkstorage::CHANNEL_MANAGER_KEY; use crate::nodemanager::{NodeStorage, DEVICE_LOCK_INTERVAL_SECS}; use crate::utils::{now, spawn}; use crate::vss::{MutinyVssClient, VssKeyValueItem}; +use crate::{ + encrypt::{decrypt_with_password, encrypt, encryption_key_from_pass, Cipher}, + federation::FederationStorage, +}; use async_trait::async_trait; use bdk::chain::{Append, PersistBackend}; use bip39::Mnemonic; @@ -19,6 +22,7 @@ pub const KEYCHAIN_STORE_KEY: &str = "bdk_keychain"; pub const MNEMONIC_KEY: &str = "mnemonic"; pub(crate) const NEED_FULL_SYNC_KEY: &str = "needs_full_sync"; pub const NODES_KEY: &str = "nodes"; +pub const FEDERATIONS_KEY: &str = "federations"; const FEE_ESTIMATES_KEY: &str = "fee_estimates"; pub const BITCOIN_PRICE_CACHE_KEY: &str = "bitcoin_price_cache"; const FIRST_SYNC_KEY: &str = "first_sync"; @@ -345,6 +349,21 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { self.set_data(NODES_KEY.to_string(), nodes, version) } + /// Gets the federation indexes from storage + fn get_federations(&self) -> Result { + let res: Option = self.get_data(FEDERATIONS_KEY)?; + match res { + Some(f) => Ok(f), + None => Ok(FederationStorage::default()), + } + } + + /// Inserts the federation indexes into storage + fn insert_federations(&self, federations: FederationStorage) -> Result<(), MutinyError> { + let version = Some(federations.version); + self.set_data(FEDERATIONS_KEY.to_string(), federations, version) + } + /// Get the current fee estimates from storage /// The key is block target, the value is the fee in satoshis per byte fn get_fee_estimates(&self) -> Result>, MutinyError> { diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index e43d3d16c..473f814b5 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -22,9 +22,10 @@ use bitcoin::hashes::sha256; use bitcoin::secp256k1::PublicKey; use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{Address, Network, OutPoint, Transaction, Txid}; +use fedimint_core::{api::InviteCode, config::FederationId}; use futures::lock::Mutex; use gloo_utils::format::JsValueSerdeExt; -use lightning::routing::gossip::NodeId; +use lightning::{log_error, routing::gossip::NodeId, util::logger::Logger}; use lightning_invoice::Bolt11Invoice; use lnurl::lightning_address::LightningAddress; use lnurl::lnurl::LnUrl; @@ -1000,6 +1001,43 @@ impl MutinyWallet { Ok(JsValue::from_serde(&activity)?) } + /// Adds a new federation based on its federation code + #[wasm_bindgen] + pub async fn new_federation( + &self, + federation_code: String, + ) -> Result { + Ok(self + .inner + .new_federation(InviteCode::from_str(&federation_code).map_err(|e| { + log_error!( + self.inner.logger, + "Error parsing federation code ({federation_code}): {e}" + ); + MutinyJsError::InvalidArgumentsError + })?) + .await? + .into()) + } + + /// Lists the federation id's of the federation clients in the manager. + #[wasm_bindgen] + pub async fn list_federations(&self) -> Result */, MutinyJsError> { + Ok(JsValue::from_serde(&self.inner.list_federations().await?)?) + } + + /// Removes a federation by setting its archived status to true, based on the FederationId. + #[wasm_bindgen] + pub async fn remove_federation(&self, federation_id: String) -> Result<(), MutinyJsError> { + Ok(self + .inner + .remove_federation( + FederationId::from_str(&federation_id) + .map_err(|_| MutinyJsError::InvalidArgumentsError)?, + ) + .await?) + } + /// Initiates a redshift #[wasm_bindgen] pub async fn init_redshift( diff --git a/mutiny-wasm/src/models.rs b/mutiny-wasm/src/models.rs index 54334a799..1ef651244 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -492,6 +492,42 @@ impl From for NodeIdentity { } } +// This is the FederationIdentity that refer to a specific node +// Used for public facing identification. +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +#[wasm_bindgen] +pub struct FederationIdentity { + uuid: String, + federation_id: String, +} + +#[wasm_bindgen] +impl FederationIdentity { + #[wasm_bindgen(getter)] + pub fn value(&self) -> JsValue { + JsValue::from_serde(&serde_json::to_value(self).unwrap()).unwrap() + } + + #[wasm_bindgen(getter)] + pub fn uuid(&self) -> String { + self.uuid.clone() + } + + #[wasm_bindgen(getter)] + pub fn federation_code(&self) -> String { + self.federation_id.to_string() + } +} + +impl From for FederationIdentity { + fn from(m: crate::models::federation::FederationIdentity) -> Self { + FederationIdentity { + uuid: m.uuid, + federation_id: m.federation_id.to_string(), + } + } +} + #[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] #[wasm_bindgen] pub struct MutinyBip21RawMaterials { From 1e5a08e9d885a4988899d851101757e93ceb5130 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Mon, 27 Nov 2023 15:03:52 -0600 Subject: [PATCH 04/12] Create or pay invoice from federation --- mutiny-core/src/federation.rs | 144 +++++++++++++++++++++++++++++++++- mutiny-core/src/lib.rs | 59 ++++++++++++-- 2 files changed, 192 insertions(+), 11 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index 778fc7745..2b048d01c 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -2,8 +2,10 @@ use crate::{ error::MutinyError, key::{create_root_child_key, ChildKey}, logging::MutinyLogger, + nodemanager::MutinyInvoice, onchain::coin_type_from_network, storage::MutinyStorage, + HTLCStatus, }; use bip39::Mnemonic; use bitcoin::{secp256k1::Secp256k1, util::bip32::ExtendedPrivKey}; @@ -17,19 +19,63 @@ use fedimint_client::{ secret::{get_default_client_secret, RootSecretStrategy}, ClientArc, FederationInfo, }; -use fedimint_core::db::mem_impl::MemDatabase; -use fedimint_core::{api::InviteCode, config::FederationId}; -use fedimint_ln_client::LightningClientInit; +use fedimint_core::{api::InviteCode, config::FederationId, db::mem_impl::MemDatabase, Amount}; +use fedimint_ln_client::{ + InternalPayState, LightningClientInit, LightningClientModule, LnPayState, LnReceiveState, +}; use fedimint_mint_client::MintClientInit; use fedimint_wallet_client::WalletClientInit; -use lightning::log_info; +use futures_util::StreamExt; use lightning::util::logger::Logger; +use lightning::{log_debug, log_info}; +use lightning_invoice::Bolt11Invoice; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, sync::{atomic::AtomicBool, Arc, RwLock}, }; +impl From for HTLCStatus { + fn from(state: LnReceiveState) -> Self { + match state { + LnReceiveState::Created => HTLCStatus::Pending, + LnReceiveState::Claimed => HTLCStatus::Succeeded, + LnReceiveState::WaitingForPayment { .. } => HTLCStatus::Pending, + LnReceiveState::Canceled { .. } => HTLCStatus::Failed, + LnReceiveState::Funded => HTLCStatus::InFlight, + LnReceiveState::AwaitingFunds => HTLCStatus::InFlight, + } + } +} + +impl From for HTLCStatus { + fn from(state: InternalPayState) -> Self { + match state { + InternalPayState::Funding => HTLCStatus::InFlight, + InternalPayState::Preimage(_) => HTLCStatus::Succeeded, + InternalPayState::RefundSuccess { .. } => HTLCStatus::Failed, + InternalPayState::RefundError { .. } => HTLCStatus::Failed, + InternalPayState::FundingFailed { .. } => HTLCStatus::Failed, + InternalPayState::UnexpectedError(_) => HTLCStatus::Failed, + } + } +} + +impl From for HTLCStatus { + fn from(state: LnPayState) -> Self { + match state { + LnPayState::Created => HTLCStatus::Pending, + LnPayState::Canceled => HTLCStatus::Failed, + LnPayState::Funded => HTLCStatus::InFlight, + LnPayState::WaitingForRefund { .. } => HTLCStatus::InFlight, + LnPayState::AwaitingChange => HTLCStatus::InFlight, + LnPayState::Success { .. } => HTLCStatus::Succeeded, + LnPayState::Refunded { .. } => HTLCStatus::Failed, + LnPayState::UnexpectedError { .. } => HTLCStatus::Failed, + } + } +} + // This is the FederationStorage object saved to the DB #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct FederationStorage { @@ -118,6 +164,96 @@ impl FederationClient { stop, }) } + + pub(crate) async fn get_invoice(&self, amount: u64) -> Result { + let lightning_module = self + .fedimint_client + .get_first_module::(); + let (_id, invoice) = lightning_module + .create_bolt11_invoice(Amount::from_sats(amount), String::new(), None, ()) + .await?; + Ok(invoice.into()) + } + + /// Get the balance of this fedimint client in sats + pub(crate) async fn get_balance(&self) -> Result { + Ok(self.fedimint_client.get_balance().await.msats / 1_000) + } + + pub(crate) async fn pay_invoice( + &self, + invoice: Bolt11Invoice, + ) -> Result { + let lightning_module = self + .fedimint_client + .get_first_module::(); + let outgoing_payment = lightning_module + .pay_bolt11_invoice(invoice.clone(), ()) + .await?; + + let mut inv: MutinyInvoice = invoice.clone().into(); + match outgoing_payment.payment_type { + fedimint_ln_client::PayType::Internal(pay_id) => { + // TODO merge the two as much as we can + let pay_outcome = lightning_module + .subscribe_internal_pay(pay_id) + .await + .map_err(|_| MutinyError::ConnectionFailed)?; + + match pay_outcome { + fedimint_client::oplog::UpdateStreamOrOutcome::UpdateStream(mut s) => { + log_debug!( + self.logger, + "waiting for update stream on payment: {}", + pay_id + ); + while let Some(outcome) = s.next().await { + log_info!(self.logger, "Outcome: {outcome:?}"); + inv.status = outcome.into(); + + if matches!(inv.status, HTLCStatus::Failed | HTLCStatus::Succeeded) { + break; + } + } + } + fedimint_client::oplog::UpdateStreamOrOutcome::Outcome(o) => { + log_info!(self.logger, "Outcome: {o:?}"); + inv.status = o.into(); + } + } + } + fedimint_ln_client::PayType::Lightning(pay_id) => { + let pay_outcome = lightning_module + .subscribe_ln_pay(pay_id) + .await + .map_err(|_| MutinyError::ConnectionFailed)?; + + match pay_outcome { + fedimint_client::oplog::UpdateStreamOrOutcome::UpdateStream(mut s) => { + log_debug!( + self.logger, + "waiting for update stream on payment: {}", + pay_id + ); + while let Some(outcome) = s.next().await { + log_info!(self.logger, "Outcome: {outcome:?}"); + inv.status = outcome.into(); + + if matches!(inv.status, HTLCStatus::Failed | HTLCStatus::Succeeded) { + break; + } + } + } + fedimint_client::oplog::UpdateStreamOrOutcome::Outcome(o) => { + log_info!(self.logger, "Outcome: {o:?}"); + inv.status = o.into(); + } + } + } + } + + Ok(inv) + } } // A federation private key will be derived from diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 2b5887eca..46e8eedea 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -518,8 +518,9 @@ impl MutinyWallet { }); } - /// Pays a lightning invoice from the selected node. + /// 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. /// The amount should be in satoshis. pub async fn pay_invoice( &self, @@ -531,6 +532,31 @@ impl MutinyWallet { return Err(MutinyError::IncorrectNetwork(inv.network())); } + // Check the amount specified in the invoice, we need one to make the payment + let send_msat = inv + .amount_milli_satoshis() + .or(amt_sats.map(|x| x * 1_000)) + .ok_or(MutinyError::InvoiceInvalid)?; + + // Try each federation first + let federation_ids = self.list_federations().await?; + for federation_id in federation_ids { + if let Some(fedimint_client) = self.federations.lock().await.get(&federation_id) { + // Check if the federation has enough balance + let balance = fedimint_client.get_balance().await?; + if balance >= send_msat / 1_000 { + // Try to pay the invoice using the federation + let payment_result = fedimint_client.pay_invoice(inv.clone()).await; + if payment_result.is_ok() { + return payment_result; + } + } + } + // If payment fails or invoice amount is None or balance is not sufficient, continue to next federation + } + // If federation client is not found, continue to next federation + + // If no federation could pay the invoice, fall back to using node_manager for payment self.node_manager .pay_invoice(None, inv, amt_sats, labels) .await @@ -567,15 +593,34 @@ impl MutinyWallet { amount: Option, labels: Vec, ) -> Result { - // If we are in safe mode, we don't create invoices let invoice = if self.config.safe_mode { None } else { - let inv = self - .node_manager - .create_invoice(amount, labels.clone()) - .await?; - Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?) + // Check if a federation exists + let federation_ids = self.list_federations().await?; + if !federation_ids.is_empty() { + // Use the first federation for simplicity + let federation_id = &federation_ids[0]; + let fedimint_client = self.federations.lock().await.get(federation_id).cloned(); + + match fedimint_client { + Some(client) => { + // Try to create an invoice using the federation + match client.get_invoice(amount.unwrap_or_default()).await { + Ok(inv) => Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?), + Err(_) => None, // Handle the error or fallback to node_manager invoice creation + } + } + None => None, // No federation client found, fallback to node_manager invoice creation + } + } else { + // Fallback to node_manager invoice creation if no federation is found + let inv = self + .node_manager + .create_invoice(amount, labels.clone()) + .await?; + Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?) + } }; let Ok(address) = self.node_manager.get_new_address(labels.clone()) else { From fcc2c1a242922bf7bd7efd016b2e4a243ec16b6c Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Fri, 1 Dec 2023 01:12:28 -0600 Subject: [PATCH 05/12] Get balance from federations --- mutiny-core/src/federation.rs | 18 +++++++-- mutiny-core/src/lib.rs | 59 ++++++++++++++++++++++++++-- mutiny-wasm/src/lib.rs | 6 +++ mutiny-wasm/src/models.rs | 72 +++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 7 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index 2b048d01c..b697b7626 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -85,6 +85,7 @@ pub struct FederationStorage { // This is the FederationIdentity that refer to a specific federation // Used for public facing identification. +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct FederationIdentity { pub uuid: String, pub federation_id: FederationId, @@ -96,10 +97,14 @@ pub struct FederationIndex { pub federation_code: InviteCode, } +pub struct FedimintBalance { + pub amount: u64, +} + // TODO remove #[allow(dead_code)] pub(crate) struct FederationClient { - pub(crate) _uuid: String, + pub(crate) uuid: String, pub(crate) federation_index: FederationIndex, pub(crate) federation_code: InviteCode, pub(crate) fedimint_client: ClientArc, @@ -153,7 +158,7 @@ impl FederationClient { .await?; Ok(FederationClient { - _uuid: uuid, + uuid, federation_index: federation_index.clone(), federation_code, fedimint_client, @@ -175,7 +180,7 @@ impl FederationClient { Ok(invoice.into()) } - /// Get the balance of this fedimint client in sats + /// Get the balance of this federation client in sats pub(crate) async fn get_balance(&self) -> Result { Ok(self.fedimint_client.get_balance().await.msats / 1_000) } @@ -254,6 +259,13 @@ impl FederationClient { Ok(inv) } + + pub async fn get_mutiny_federation_identity(&self) -> FederationIdentity { + FederationIdentity { + uuid: self.uuid.clone(), + federation_id: self.fedimint_client.federation_id(), + } + } } // A federation private key will be derived from diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 46e8eedea..a13c17e99 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -92,20 +92,33 @@ pub struct MutinyBalance { pub confirmed: u64, pub unconfirmed: u64, pub lightning: u64, + pub federation: u64, pub force_close: u64, } impl MutinyBalance { - fn new(ln_balance: NodeBalance) -> Self { + fn new(ln_balance: NodeBalance, federation_balance: u64) -> Self { Self { confirmed: ln_balance.confirmed, unconfirmed: ln_balance.unconfirmed, lightning: ln_balance.lightning, + federation: federation_balance, force_close: ln_balance.force_close, } } } +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct FederationBalance { + pub identity: FederationIdentity, + pub balance: u64, +} + +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct FederationBalances { + pub balances: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub enum ActivityItem { OnChain(TransactionDetails), @@ -636,11 +649,14 @@ impl MutinyWallet { } /// Gets the current balance of the wallet. - /// This includes both on-chain and lightning funds. + /// This includes both on-chain, lightning funds, and federations. /// /// This will not include any funds in an unconfirmed lightning channel. pub async fn get_balance(&self) -> Result { - Ok(MutinyBalance::new(self.node_manager.get_balance().await?)) + let ln_balance = self.node_manager.get_balance().await?; + let federation_balance = self.get_total_federation_balance().await?; + + Ok(MutinyBalance::new(ln_balance, federation_balance)) } /// Get the sorted activity list for lightning payments, channels, and txs. @@ -978,7 +994,7 @@ impl MutinyWallet { let mut federations_guard = self.federations.lock().await; if let Some(fedimint_client) = federations_guard.get(&federation_id) { - let uuid = &fedimint_client._uuid; + let uuid = &fedimint_client.uuid; let mut federation_storage_guard = self.federation_storage.lock().await; @@ -996,6 +1012,41 @@ impl MutinyWallet { Ok(()) } + + pub async fn get_total_federation_balance(&self) -> Result { + let federation_ids = self.list_federations().await?; + let mut total_balance = 0; + + let federations = self.federations.lock().await; + for fed_id in federation_ids { + let balance = federations + .get(&fed_id) + .ok_or(MutinyError::NotFound)? + .get_balance() + .await?; + + total_balance += balance; + } + + Ok(total_balance) + } + + pub async fn get_federation_balances(&self) -> Result { + let federation_lock = self.federations.lock().await; + + let federation_ids = self.list_federations().await?; + let mut balances = Vec::with_capacity(federation_ids.len()); + for fed_id in federation_ids { + let fedimint_client = federation_lock.get(&fed_id).ok_or(MutinyError::NotFound)?; + + let balance = fedimint_client.get_balance().await?; + let identity = fedimint_client.get_mutiny_federation_identity().await; + + balances.push(FederationBalance { identity, balance }); + } + + Ok(FederationBalances { balances }) + } } async fn create_federations( diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 473f814b5..af400c405 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -1038,6 +1038,12 @@ impl MutinyWallet { .await?) } + /// Gets the current balances of each federation. + #[wasm_bindgen] + pub async fn get_federation_balances(&self) -> Result { + Ok(self.inner.get_federation_balances().await?.into()) + } + /// Initiates a redshift #[wasm_bindgen] pub async fn init_redshift( diff --git a/mutiny-wasm/src/models.rs b/mutiny-wasm/src/models.rs index 1ef651244..3793086f7 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -403,6 +403,7 @@ pub struct MutinyBalance { pub confirmed: u64, pub unconfirmed: u64, pub lightning: u64, + pub federation: u64, pub force_close: u64, } @@ -420,11 +421,82 @@ impl From for MutinyBalance { confirmed: m.confirmed, unconfirmed: m.unconfirmed, lightning: m.lightning, + federation: m.federation, force_close: m.force_close, } } } +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +#[wasm_bindgen] +pub struct FederationBalance { + identity: FederationIdentity, + balance: u64, +} + +#[wasm_bindgen] +impl FederationBalance { + #[wasm_bindgen(getter)] + pub fn value(&self) -> JsValue { + JsValue::from_serde(&serde_json::to_value(self).unwrap()).unwrap() + } + + #[wasm_bindgen(getter)] + pub fn identity_uuid(&self) -> String { + self.identity.uuid.clone() + } + + #[wasm_bindgen(getter)] + pub fn identity_federation_id(&self) -> String { + self.identity.federation_id.clone() + } + + #[wasm_bindgen(getter)] + pub fn balance(&self) -> u64 { + self.balance + } +} + +impl From for FederationBalance { + fn from(core_balance: mutiny_core::FederationBalance) -> Self { + FederationBalance { + identity: core_balance.identity.into(), + balance: core_balance.balance, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +#[wasm_bindgen] +pub struct FederationBalances { + balances: Vec, +} + +#[wasm_bindgen] +impl FederationBalances { + #[wasm_bindgen(getter)] + pub fn value(&self) -> JsValue { + JsValue::from_serde(&serde_json::to_value(self).unwrap()).unwrap() + } + + #[wasm_bindgen(getter)] + pub fn balances(&self) -> Vec { + self.balances.clone() + } +} + +impl From for FederationBalances { + fn from(core_balances: mutiny_core::FederationBalances) -> Self { + FederationBalances { + balances: core_balances + .balances + .into_iter() + .map(FederationBalance::from) + .collect(), + } + } +} + #[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] #[wasm_bindgen] pub struct LnUrlParams { From cd7d31bc10510aba4bf66ff41ab241a0d0ced1ef Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Wed, 6 Dec 2023 19:27:19 -0600 Subject: [PATCH 06/12] Pull in activity list from federation --- mutiny-core/src/federation.rs | 393 ++++++++++++++++++++++++++++----- mutiny-core/src/lib.rs | 51 ++++- mutiny-core/src/nodemanager.rs | 3 - 3 files changed, 377 insertions(+), 70 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index b697b7626..b6061297c 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -5,36 +5,62 @@ use crate::{ nodemanager::MutinyInvoice, onchain::coin_type_from_network, storage::MutinyStorage, - HTLCStatus, + utils::sleep, + ActivityItem, HTLCStatus, DEFAULT_PAYMENT_TIMEOUT, }; use bip39::Mnemonic; -use bitcoin::{secp256k1::Secp256k1, util::bip32::ExtendedPrivKey}; use bitcoin::{ + hashes::{hex::ToHex, sha256}, + secp256k1::Secp256k1, + util::bip32::ExtendedPrivKey, util::bip32::{ChildNumber, DerivationPath}, Network, }; use fedimint_bip39::Bip39RootSecretStrategy; use fedimint_client::{ derivable_secret::DerivableSecret, + oplog::{OperationLogEntry, UpdateStreamOrOutcome}, secret::{get_default_client_secret, RootSecretStrategy}, ClientArc, FederationInfo, }; -use fedimint_core::{api::InviteCode, config::FederationId, db::mem_impl::MemDatabase, Amount}; +use fedimint_core::{ + api::InviteCode, + config::FederationId, + core::OperationId, + db::mem_impl::MemDatabase, + module::CommonModuleInit, + task::{MaybeSend, MaybeSync}, + Amount, +}; use fedimint_ln_client::{ - InternalPayState, LightningClientInit, LightningClientModule, LnPayState, LnReceiveState, + InternalPayState, LightningClientInit, LightningClientModule, LightningOperationMeta, + LightningOperationMetaVariant, LnPayState, LnReceiveState, }; +use fedimint_ln_common::LightningCommonInit; use fedimint_mint_client::MintClientInit; use fedimint_wallet_client::WalletClientInit; -use futures_util::StreamExt; -use lightning::util::logger::Logger; -use lightning::{log_debug, log_info}; +use futures::future::{self}; +use futures_util::{pin_mut, stream, StreamExt}; +use lightning::{log_debug, log_info, log_warn, util::logger::Logger}; use lightning_invoice::Bolt11Invoice; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ collections::HashMap, + fmt::Debug, sync::{atomic::AtomicBool, Arc, RwLock}, }; +// The amount of time in milliseconds to wait for +// checking the status of a fedimint payment. This +// is to work around their stream status checking +// when wanting just the current status. +const FEDIMINT_STATUS_TIMEOUT_CHECK_MS: u64 = 30; + +// The maximum amount of operations we try to pull +// from fedimint when we need to search through +// their internal list. +const FEDIMINT_OPERATIONS_LIST_MAX: usize = 100; + impl From for HTLCStatus { fn from(state: LnReceiveState) -> Self { match state { @@ -185,6 +211,86 @@ impl FederationClient { Ok(self.fedimint_client.get_balance().await.msats / 1_000) } + pub async fn get_activity(&self) -> Result, MutinyError> { + let operations = self + .fedimint_client + .operation_log() + .list_operations(FEDIMINT_OPERATIONS_LIST_MAX, None) + .await; + + let lightning_module = Arc::new( + self.fedimint_client + .get_first_module::(), + ); + + let activity_stream = stream::iter(operations.into_iter()); + + let activity_items = activity_stream + .then(move |(key, entry)| { + let logger = self.logger.clone(); + let lightning_module = Arc::clone(&lightning_module); + async move { + if entry.operation_module_kind() == LightningCommonInit::KIND.as_str() { + Some( + handle_ln_meta(logger, &entry, key.operation_id, &lightning_module) + .await, + ) + } else { + log_warn!( + logger, + "Unsupported module: {}", + entry.operation_module_kind() + ); + None + } + } + }) + .filter_map(|item| async move { item }) + .collect::>() + .await; + + Ok(activity_items) + } + + pub async fn get_invoice_by_hash( + &self, + hash: &sha256::Hash, + ) -> Result { + let operations = self + .fedimint_client + .operation_log() + .list_operations(FEDIMINT_OPERATIONS_LIST_MAX, None) + .await; + + let lightning_module = self + .fedimint_client + .get_first_module::(); + + for (key, entry) in operations { + if entry.operation_module_kind() == LightningCommonInit::KIND.as_str() { + if let Some(invoice) = extract_invoice_from_entry( + self.logger.clone(), + &entry, + hash, + key.operation_id, + &lightning_module, + ) + .await + { + return Ok(invoice); + } + } else { + log_warn!( + self.logger, + "Unsupported module: {}", + entry.operation_module_kind() + ); + } + } + + Err(MutinyError::NotFound) + } + pub(crate) async fn pay_invoice( &self, invoice: Bolt11Invoice, @@ -196,68 +302,48 @@ impl FederationClient { .pay_bolt11_invoice(invoice.clone(), ()) .await?; - let mut inv: MutinyInvoice = invoice.clone().into(); - match outgoing_payment.payment_type { + // Subscribe and process outcome based on payment type + let inv = match outgoing_payment.payment_type { fedimint_ln_client::PayType::Internal(pay_id) => { - // TODO merge the two as much as we can - let pay_outcome = lightning_module - .subscribe_internal_pay(pay_id) - .await - .map_err(|_| MutinyError::ConnectionFailed)?; - - match pay_outcome { - fedimint_client::oplog::UpdateStreamOrOutcome::UpdateStream(mut s) => { - log_debug!( - self.logger, - "waiting for update stream on payment: {}", - pay_id - ); - while let Some(outcome) = s.next().await { - log_info!(self.logger, "Outcome: {outcome:?}"); - inv.status = outcome.into(); - - if matches!(inv.status, HTLCStatus::Failed | HTLCStatus::Succeeded) { - break; - } - } - } - fedimint_client::oplog::UpdateStreamOrOutcome::Outcome(o) => { - log_info!(self.logger, "Outcome: {o:?}"); - inv.status = o.into(); + match lightning_module.subscribe_internal_pay(pay_id).await { + Ok(o) => { + process_outcome( + o, + process_pay_state_internal, + invoice.clone(), + true, + DEFAULT_PAYMENT_TIMEOUT * 1_000, + Arc::clone(&self.logger), + ) + .await } + Err(_) => invoice.clone().into(), } } fedimint_ln_client::PayType::Lightning(pay_id) => { - let pay_outcome = lightning_module - .subscribe_ln_pay(pay_id) - .await - .map_err(|_| MutinyError::ConnectionFailed)?; - - match pay_outcome { - fedimint_client::oplog::UpdateStreamOrOutcome::UpdateStream(mut s) => { - log_debug!( - self.logger, - "waiting for update stream on payment: {}", - pay_id - ); - while let Some(outcome) = s.next().await { - log_info!(self.logger, "Outcome: {outcome:?}"); - inv.status = outcome.into(); - - if matches!(inv.status, HTLCStatus::Failed | HTLCStatus::Succeeded) { - break; - } - } - } - fedimint_client::oplog::UpdateStreamOrOutcome::Outcome(o) => { - log_info!(self.logger, "Outcome: {o:?}"); - inv.status = o.into(); + match lightning_module.subscribe_ln_pay(pay_id).await { + Ok(o) => { + process_outcome( + o, + process_pay_state_ln, + invoice.clone(), + false, + DEFAULT_PAYMENT_TIMEOUT * 1_000, + Arc::clone(&self.logger), + ) + .await } + Err(_) => invoice.clone().into(), } } - } + }; - Ok(inv) + match inv.status { + HTLCStatus::Succeeded => Ok(inv), + HTLCStatus::Failed => Err(MutinyError::RoutingFailed), + HTLCStatus::Pending => Err(MutinyError::PaymentTimeout), + HTLCStatus::InFlight => Err(MutinyError::PaymentTimeout), + } } pub async fn get_mutiny_federation_identity(&self) -> FederationIdentity { @@ -298,6 +384,191 @@ pub(crate) fn mnemonic_from_xpriv(xpriv: ExtendedPrivKey) -> Result, + entry: &OperationLogEntry, + hash: &sha256::Hash, + operation_id: OperationId, + lightning_module: &LightningClientModule, +) -> Option { + let lightning_meta: LightningOperationMeta = entry.meta(); + + match lightning_meta.variant { + LightningOperationMetaVariant::Pay(pay_meta) => { + if pay_meta.invoice.payment_hash() == hash { + match lightning_module.subscribe_ln_pay(operation_id).await { + Ok(o) => Some( + process_outcome( + o, + process_pay_state_ln, + pay_meta.invoice, + false, + FEDIMINT_STATUS_TIMEOUT_CHECK_MS, + logger, + ) + .await, + ), + Err(_) => Some(pay_meta.invoice.into()), + } + } else { + None + } + } + LightningOperationMetaVariant::Receive { invoice, .. } => { + if invoice.payment_hash() == hash { + match lightning_module.subscribe_ln_receive(operation_id).await { + Ok(o) => Some( + process_outcome( + o, + process_receive_state, + invoice, + true, + FEDIMINT_STATUS_TIMEOUT_CHECK_MS, + logger, + ) + .await, + ), + Err(_) => Some(invoice.into()), + } + } else { + None + } + } + } +} + +async fn handle_ln_meta( + logger: Arc, + entry: &OperationLogEntry, + operation_id: OperationId, + lightning_module: &LightningClientModule, +) -> ActivityItem { + let lightning_meta: LightningOperationMeta = entry.meta(); + + let invoice = match lightning_meta.variant { + LightningOperationMetaVariant::Pay(pay_meta) => { + match lightning_module.subscribe_ln_pay(operation_id).await { + Ok(o) => { + process_outcome( + o, + process_pay_state_ln, + pay_meta.invoice, + false, + FEDIMINT_STATUS_TIMEOUT_CHECK_MS, + logger, + ) + .await + } + Err(_) => pay_meta.invoice.into(), + } + } + LightningOperationMetaVariant::Receive { invoice, .. } => { + match lightning_module.subscribe_ln_receive(operation_id).await { + Ok(o) => { + process_outcome( + o, + process_receive_state, + invoice, + true, + FEDIMINT_STATUS_TIMEOUT_CHECK_MS, + logger, + ) + .await + } + Err(_) => invoice.into(), + } + } + }; + + ActivityItem::Lightning(Box::new(invoice)) +} + +fn process_pay_state_internal(pay_state: InternalPayState) -> (HTLCStatus, Option) { + let status: HTLCStatus = pay_state.clone().into(); + + let p = if let InternalPayState::Preimage(preimage) = pay_state { + Some(preimage.0.to_hex()) + } else { + None + }; + + (status, p) +} + +fn process_pay_state_ln(pay_state: LnPayState) -> (HTLCStatus, Option) { + let status: HTLCStatus = pay_state.clone().into(); + + let p = if let LnPayState::Success { ref preimage } = pay_state { + Some(preimage.to_string()) + } else { + None + }; + + (status, p) +} + +fn process_receive_state(receive_state: LnReceiveState) -> (HTLCStatus, Option) { + let status: HTLCStatus = receive_state.into(); + (status, None) +} + +async fn process_outcome( + stream_or_outcome: UpdateStreamOrOutcome, + process_fn: F, + invoice: Bolt11Invoice, + inbound: bool, + timeout: u64, + logger: Arc, +) -> MutinyInvoice +where + U: Into + + Clone + + Serialize + + DeserializeOwned + + Debug + + MaybeSend + + MaybeSync + + 'static, + F: Fn(U) -> (HTLCStatus, Option) + Copy, +{ + let mut invoice: MutinyInvoice = invoice.into(); + invoice.inbound = inbound; + + match stream_or_outcome { + UpdateStreamOrOutcome::Outcome(outcome) => { + invoice.status = outcome.into(); + log_debug!(logger, "Outcome received: {}", invoice.status); + } + UpdateStreamOrOutcome::UpdateStream(mut s) => { + let timeout_future = sleep(timeout as i32); + pin_mut!(timeout_future); + + while let future::Either::Left((outcome_option, _)) = + future::select(s.next(), &mut timeout_future).await + { + if let Some(outcome) = outcome_option { + let (status, preimage) = process_fn(outcome); + invoice.status = status; + invoice.preimage = preimage; + + if matches!(invoice.status, HTLCStatus::Succeeded | HTLCStatus::Failed) { + break; + } + } else { + log_debug!( + logger, + "Timeout reached, exiting loop for payment {}", + invoice.payment_hash + ); + break; + } + } + } + } + + invoice +} + #[cfg(test)] fn fedimint_seed_generation() { use crate::generate_seed; diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index a13c17e99..b4b5116c0 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -560,14 +560,27 @@ impl MutinyWallet { if balance >= send_msat / 1_000 { // Try to pay the invoice using the federation let payment_result = fedimint_client.pay_invoice(inv.clone()).await; - if payment_result.is_ok() { - return payment_result; + match payment_result { + Ok(r) => return Ok(r), + Err(e) => match e { + MutinyError::PaymentTimeout => return Err(e), + MutinyError::RoutingFailed => { + log_debug!( + self.logger, + "could not make payment through federation: {e}" + ); + continue; + } + _ => { + log_warn!(self.logger, "unhandled error: {e}") + } + }, } } + // If payment fails or invoice amount is None or balance is not sufficient, continue to next federation } - // If payment fails or invoice amount is None or balance is not sufficient, continue to next federation + // If federation client is not found, continue to next federation } - // If federation client is not found, continue to next federation // If no federation could pay the invoice, fall back to using node_manager for payment self.node_manager @@ -661,7 +674,20 @@ impl MutinyWallet { /// Get the sorted activity list for lightning payments, channels, and txs. pub async fn get_activity(&self) -> Result, MutinyError> { - self.node_manager.get_activity().await + // Get activities from node manager + let mut activities = self.node_manager.get_activity().await?; + + // Directly iterate over federation clients to get their activities + let federations = self.federations.lock().await; + for (_fed_id, federation) in federations.iter() { + let federation_activities = federation.get_activity().await?; + activities.extend(federation_activities); + } + + // Sort all activities, newest first + activities.sort_by(|a, b| b.cmp(a)); + + Ok(activities) } /// Gets an invoice. @@ -676,7 +702,20 @@ impl MutinyWallet { &self, hash: &sha256::Hash, ) -> Result { - self.node_manager.get_invoice_by_hash(hash).await + // 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 not found in node manager, search in federations + let federations = self.federations.lock().await; + for (_fed_id, federation) in federations.iter() { + if let Ok(invoice) = federation.get_invoice_by_hash(hash).await { + return Ok(invoice); + } + } + + Err(MutinyError::NotFound) } /// Checks whether or not the user is subscribed to Mutiny+. diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index dcb1c456e..0155979d8 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -1166,9 +1166,6 @@ impl NodeManager { activity.push(ActivityItem::ChannelClosed(chan)); } - // Newest first - activity.sort_by(|a, b| b.cmp(a)); - Ok(activity) } From 49e7c805be06830a5350e31f8a0f8ef8c781939e Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Tue, 12 Dec 2023 12:34:06 -0600 Subject: [PATCH 07/12] Add gluesql library and implement fedimint storage --- Cargo.lock | 512 ++++++++++++++++++++++++++++- mutiny-core/Cargo.toml | 4 + mutiny-core/src/federation.rs | 18 +- mutiny-core/src/lib.rs | 21 +- mutiny-core/src/sql/glue.rs | 594 ++++++++++++++++++++++++++++++++++ mutiny-core/src/sql/mod.rs | 1 + 6 files changed, 1139 insertions(+), 11 deletions(-) create mode 100644 mutiny-core/src/sql/glue.rs create mode 100644 mutiny-core/src/sql/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 1793e3be0..e5383b9f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,17 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" +[[package]] +name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -316,6 +327,20 @@ dependencies = [ "serde", ] +[[package]] +name = "bigdecimal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06619be423ea5bb86c95f087d5707942791a08a85530df0db2209a3ecfb8bc9" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -440,6 +465,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -502,6 +536,30 @@ dependencies = [ "zeroize", ] +[[package]] +name = "borsh" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9897ef0f1bd2362169de6d7e436ea2237dc1085d7d1e4db75f4be34d86f309d1" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478b41ff04256c5c8330f3dfdaaae2a5cc976a8e75088bafa4625b0d0208de8c" +dependencies = [ + "once_cell", + "proc-macro-crate 2.0.0", + "proc-macro2", + "quote", + "syn 2.0.39", + "syn_derive", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -514,6 +572,28 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" +[[package]] +name = "bytecheck" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -550,6 +630,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chacha20" version = "0.9.1" @@ -689,6 +775,28 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "derive_utils" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_utils" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abcad25e9720609ccb3dcdb795d845e37d8ce34183330a9f48b03a1a71c8e21" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "difflib" version = "0.4.0" @@ -1193,6 +1301,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -1275,6 +1392,18 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +[[package]] +name = "futures-enum" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3422d14de7903a52e9dbc10ae05a7e14445ec61890100e098754e120b2bd7b1e" +dependencies = [ + "derive_utils 0.11.2", + "find-crate", + "quote", + "syn 1.0.109", +] + [[package]] name = "futures-executor" version = "0.3.29" @@ -1391,7 +1520,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-sink", - "gloo-utils", + "gloo-utils 0.2.0", "http", "js-sys", "pin-project", @@ -1415,6 +1544,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-utils" version = "0.2.0" @@ -1428,6 +1570,89 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gluesql" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45dfe66dc370c1d234bf8d0a22b7e62062af674f304ed3e347e3ad4e8f8224d3" +dependencies = [ + "gluesql-core", + "gluesql-idb-storage", + "gluesql_memory_storage", +] + +[[package]] +name = "gluesql-core" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9caaf3cf33c4fc065da4b769dfc8d594a440b459dc38652185419f671b00bc69" +dependencies = [ + "async-recursion", + "async-trait", + "bigdecimal", + "cfg-if", + "chrono", + "futures", + "futures-enum", + "gluesql-utils", + "hex", + "im-rc", + "iter-enum", + "itertools 0.10.5", + "md-5", + "ordered-float", + "rand", + "regex", + "rust_decimal", + "serde", + "serde_json", + "sqlparser", + "strum_macros", + "thiserror", + "uuid", +] + +[[package]] +name = "gluesql-idb-storage" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf438572b361f648f6204df9c0fb956db9a0cb350030d8ca2048d5cf2e48960" +dependencies = [ + "async-trait", + "futures", + "gloo-utils 0.1.7", + "gluesql-core", + "idb", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gluesql-utils" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4689be8d1ec30ee78b2e3898cd96b40b1c27fde7ec5956dcf943b66502a45571" +dependencies = [ + "futures", + "indexmap 1.9.3", + "pin-project", +] + +[[package]] +name = "gluesql_memory_storage" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ae7ac5bf854a6c966f26e58dda1bc5408260c772499731596428d12492b245" +dependencies = [ + "async-trait", + "futures", + "gluesql-core", + "serde", +] + [[package]] name = "group" version = "0.12.1" @@ -1470,7 +1695,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" dependencies = [ - "ahash", + "ahash 0.3.8", "autocfg", ] @@ -1479,6 +1704,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.7", +] [[package]] name = "hashbrown" @@ -1636,6 +1864,34 @@ dependencies = [ "cc", ] +[[package]] +name = "idb" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0dc17fcfa27c05cf62bf2f2f649d6229e08f6f12717f74ef85102a895a95a02" +dependencies = [ + "idb-sys", + "js-sys", + "num-traits", + "thiserror", + "tokio", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "idb-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc0cc5fe5831ee0a6952d35e9f6650582a76976d5219f737d8d1d718c2e0b5a" +dependencies = [ + "js-sys", + "num-traits", + "thiserror", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "idna" version = "0.4.0" @@ -1652,6 +1908,20 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "impl-tools" version = "0.8.0" @@ -1754,6 +2024,17 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "iter-enum" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83abfc4e5361c9cb087172c7f6d46a4db47e3c4fccf5d9168599451f230765cd" +dependencies = [ + "derive_utils 0.13.2", + "quote", + "syn 2.0.39", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1924,6 +2205,12 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "lightning" version = "0.0.118" @@ -2061,6 +2348,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.6.4" @@ -2152,6 +2449,7 @@ dependencies = [ "bdk-macros", "bdk_chain", "bdk_esplora", + "bincode", "bip39", "bitcoin 0.29.2", "cbc", @@ -2169,6 +2467,8 @@ dependencies = [ "futures-util", "getrandom", "gloo-net", + "gluesql", + "hex", "instant", "itertools 0.11.0", "js-sys", @@ -2212,7 +2512,7 @@ dependencies = [ "fedimint-core", "futures", "getrandom", - "gloo-utils", + "gloo-utils 0.2.0", "instant", "js-sys", "lightning", @@ -2322,6 +2622,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -2406,6 +2727,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", + "rand", + "serde", +] + [[package]] name = "overload" version = "0.1.1" @@ -2441,7 +2773,7 @@ version = "3.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "312270ee71e1cd70289dacf597cab7b207aa107d2f28191c2ae45b2ece18a260" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 1.0.109", @@ -2637,7 +2969,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", ] [[package]] @@ -2673,6 +3014,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.33" @@ -2697,6 +3058,7 @@ dependencies = [ "libc", "rand_chacha", "rand_core", + "serde", ] [[package]] @@ -2716,6 +3078,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", + "serde", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", ] [[package]] @@ -2771,6 +3143,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rend" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.22" @@ -2859,6 +3240,50 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rkyv" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +dependencies = [ + "bitvec", + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2979,6 +3404,12 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "secp256k1" version = "0.24.3" @@ -3120,6 +3551,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.192" @@ -3217,6 +3659,22 @@ dependencies = [ "libc", ] +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.9" @@ -3279,6 +3737,17 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "sqlparser" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eaa1e88e78d2c2460d78b7dc3f0c08dbb606ab4222f9aff36f420d36e307d87" +dependencies = [ + "bigdecimal", + "log", + "serde", +] + [[package]] name = "strum" version = "0.24.1" @@ -3326,6 +3795,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -3544,6 +4025,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.6.5" @@ -3561,6 +4051,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -3767,6 +4268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ "getrandom", + "wasm-bindgen", ] [[package]] diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index ee5f108cb..5e6cf5bd7 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -45,6 +45,9 @@ aes = { version = "0.8" } jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] } argon2 = { version = "0.5.0", features = ["password-hash", "alloc"] } payjoin = { version = "0.10.0", features = ["send", "base64"] } +gluesql = { version = "0.15", default-features = false, features = ["memory-storage"] } +bincode = "1.3.3" +hex = "0.4.3" fedimint-client = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } fedimint-core = { git = "https://github.com/fedimint/fedimint.git", rev = "51d09c76b8a15615114d4f2c2663c738d8ff3135" } @@ -81,6 +84,7 @@ js-sys = "0.3.65" gloo-net = { version = "0.4.0" } instant = { version = "0.1", features = ["wasm-bindgen"] } getrandom = { version = "0.2", features = ["js"] } +gluesql = { version = "0.15", default-features = false, features = ["idb-storage"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1", features = ["rt"] } diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index b6061297c..d69fa353a 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -4,6 +4,7 @@ use crate::{ logging::MutinyLogger, nodemanager::MutinyInvoice, onchain::coin_type_from_network, + sql::glue::GlueDB, storage::MutinyStorage, utils::sleep, ActivityItem, HTLCStatus, DEFAULT_PAYMENT_TIMEOUT, @@ -19,6 +20,7 @@ use bitcoin::{ use fedimint_bip39::Bip39RootSecretStrategy; use fedimint_client::{ derivable_secret::DerivableSecret, + get_config_from_db, oplog::{OperationLogEntry, UpdateStreamOrOutcome}, secret::{get_default_client_secret, RootSecretStrategy}, ClientArc, FederationInfo, @@ -27,7 +29,6 @@ use fedimint_core::{ api::InviteCode, config::FederationId, core::OperationId, - db::mem_impl::MemDatabase, module::CommonModuleInit, task::{MaybeSend, MaybeSync}, Amount, @@ -151,6 +152,7 @@ impl FederationClient { federation_code: InviteCode, xprivkey: ExtendedPrivKey, storage: S, + g: GlueDB, network: Network, logger: Arc, stop: Arc, @@ -169,10 +171,17 @@ impl FederationClient { client_builder.with_module(WalletClientInit(None)); client_builder.with_module(MintClientInit); client_builder.with_module(LightningClientInit); - client_builder.with_database(MemDatabase::new().into()); // TODO not in memory + + let db = g + .new_fedimint_client_db(federation_info.federation_id().to_string()) + .await? + .into(); + if get_config_from_db(&db).await.is_none() { + client_builder.with_federation_info(federation_info.clone()); + } + + client_builder.with_database(db); client_builder.with_primary_module(1); - client_builder - .with_federation_info(FederationInfo::from_invite_code(federation_code.clone()).await?); let secret = create_federation_secret(xprivkey, network)?; @@ -183,6 +192,7 @@ impl FederationClient { )) .await?; + log_debug!(logger, "Built fedimint client"); Ok(FederationClient { uuid, federation_index: federation_index.clone(), diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index b4b5116c0..aa8129090 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -33,6 +33,7 @@ mod onchain; mod peermanager; pub mod redshift; pub mod scorer; +pub mod sql; pub mod storage; mod subscription; pub mod utils; @@ -46,7 +47,6 @@ 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::federation::{FederationClient, FederationIdentity, FederationIndex, FederationStorage}; use crate::logging::LOGGING_KEY; use crate::nodemanager::{ ChannelClosure, MutinyBip21RawMaterials, MutinyInvoice, TransactionDetails, @@ -59,8 +59,10 @@ use crate::storage::{MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FU use crate::{auth::MutinyAuthClient, logging::MutinyLogger}; use crate::{error::MutinyError, nostr::ReservedProfile}; use crate::{ + federation::{FederationClient, FederationIdentity, FederationIndex, FederationStorage}, labels::{get_contact_key, Contact, LabelStorage}, nodemanager::NodeBalance, + sql::glue::GlueDB, }; use crate::{nodemanager::NodeManager, nostr::ProfileType}; use crate::{nostr::NostrManager, utils::sleep}; @@ -281,6 +283,7 @@ impl MutinyWalletConfig { pub struct MutinyWallet { pub config: MutinyWalletConfig, pub storage: S, + glue_db: GlueDB, pub node_manager: Arc>, pub nostr: Arc>, pub federation_storage: Arc>, @@ -330,9 +333,17 @@ impl MutinyWallet { node_manager.logger.clone(), )?); + // create gluedb storage + let glue_db = GlueDB::new( + #[cfg(target_arch = "wasm32")] + None, + logger.clone(), + ) + .await?; + // create federation library let (federation_storage, federations) = - create_federations(&storage, &config, &logger, stop.clone()).await?; + create_federations(&storage, &config, glue_db.clone(), &logger, stop.clone()).await?; let federation_storage = Arc::new(Mutex::new(federation_storage)); let federations = federations; @@ -346,6 +357,7 @@ impl MutinyWallet { let mw = Self { config, storage, + glue_db, node_manager, nostr, federation_storage, @@ -1008,6 +1020,7 @@ impl MutinyWallet { create_new_federation( self.config.xprivkey, self.storage.clone(), + self.glue_db.clone(), self.config.network, self.logger.clone(), self.federation_storage.clone(), @@ -1091,6 +1104,7 @@ impl MutinyWallet { async fn create_federations( storage: &S, c: &MutinyWalletConfig, + g: GlueDB, logger: &Arc, stop: Arc, ) -> Result< @@ -1110,6 +1124,7 @@ async fn create_federations( federation_item.1.federation_code.clone(), c.xprivkey, storage.clone(), + g.clone(), c.network, logger.clone(), stop.clone(), @@ -1129,6 +1144,7 @@ async fn create_federations( pub(crate) async fn create_new_federation( xprivkey: ExtendedPrivKey, storage: S, + g: GlueDB, network: Network, logger: Arc, federation_storage: Arc>, @@ -1174,6 +1190,7 @@ pub(crate) async fn create_new_federation( federation_code, xprivkey, storage.clone(), + g.clone(), network, logger.clone(), stop, diff --git a/mutiny-core/src/sql/glue.rs b/mutiny-core/src/sql/glue.rs new file mode 100644 index 000000000..ce109037f --- /dev/null +++ b/mutiny-core/src/sql/glue.rs @@ -0,0 +1,594 @@ +use crate::{ + error::{MutinyError, MutinyStorageError}, + logging::MutinyLogger, +}; +use async_trait::async_trait; +use fedimint_core::db::{ + mem_impl::{MemDatabase, MemTransaction}, + IDatabaseTransactionOps, IDatabaseTransactionOpsCore, IRawDatabase, IRawDatabaseTransaction, + PrefixStream, +}; +use gluesql::prelude::{Glue, Payload, Value}; +use lightning::{log_debug, util::logger::Logger}; + +use std::{fmt, sync::Arc}; + +#[cfg(not(target_arch = "wasm32"))] +use tokio::sync::Mutex; + +#[cfg(target_arch = "wasm32")] +use futures::lock::Mutex; + +#[cfg(target_arch = "wasm32")] +use futures::StreamExt; + +#[cfg(not(target_arch = "wasm32"))] +use gluesql::prelude::MemoryStorage; + +#[cfg(not(target_arch = "wasm32"))] +#[derive(Clone)] +pub struct FedimintDB { + pub(crate) db: Arc>>, // TODO eventually use sled for server version + fedimint_memory: Arc, + federation_id: String, +} + +#[cfg(not(target_arch = "wasm32"))] +#[derive(Clone)] +pub struct GlueDB { + pub(crate) db: Arc>>, // TODO eventually use sled for server version + logger: Arc, +} + +#[cfg(target_arch = "wasm32")] +use gluesql::prelude::IdbStorage; + +#[cfg(target_arch = "wasm32")] +#[derive(Clone)] +pub struct GlueDB { + pub(crate) db: Arc>>, + logger: Arc, +} + +impl GlueDB { + pub async fn new( + #[cfg(target_arch = "wasm32")] namespace: Option, + logger: Arc, + ) -> Result { + log_debug!(logger, "initializing glue storage"); + + #[cfg(target_arch = "wasm32")] + let storage = IdbStorage::new(namespace) + .await + .map_err(|_| MutinyError::ReadError { + source: MutinyStorageError::IndexedDBError, + })?; + + #[cfg(not(target_arch = "wasm32"))] + let storage = MemoryStorage::default(); + + let mut glue_db = Glue::new(storage); + glue_db + .execute( + "CREATE TABLE IF NOT EXISTS mutiny_kv (key TEXT PRIMARY KEY, val BYTEA NOT NULL, version INTEGER NOT NULL)", + ) + .await + .map_err(|_| MutinyError::write_err(MutinyStorageError::IndexedDBError))?; + + log_debug!(logger, "done setting up GlueDB"); + + Ok(Self { + db: Arc::new(Mutex::new(glue_db)), + logger, + }) + } + + pub async fn new_fedimint_client_db( + &self, + federation_id: String, + ) -> Result { + FedimintDB::new(self.db.clone(), federation_id, self.logger.clone()).await + } +} + +#[cfg(target_arch = "wasm32")] +#[derive(Clone)] +pub struct FedimintDB { + pub(crate) db: Arc>>, + fedimint_memory: Arc, + federation_id: String, +} + +impl fmt::Debug for FedimintDB { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FedimintDB").finish() + } +} + +impl FedimintDB { + pub async fn new( + #[cfg(target_arch = "wasm32")] db: Arc>>, + #[cfg(not(target_arch = "wasm32"))] db: Arc>>, + federation_id: String, + logger: Arc, + ) -> Result { + log_debug!(logger, "initializing glue storage"); + + let fedimint_memory = MemDatabase::new(); + + { + let mut glue_db = db.lock().await; + + let select_query = format!( + "SELECT val FROM mutiny_kv WHERE key = '{}'", + key_id(&federation_id) + ); + let mut result = + glue_db + .execute(select_query) + .await + .map_err(|_| MutinyError::ReadError { + source: MutinyStorageError::IndexedDBError, + })?; + + if let Payload::Select { rows, .. } = result.pop().expect("should get something") { + if rows.is_empty() { + let stmt = format!( + "INSERT INTO mutiny_kv (key, val, version) VALUES ('{}', X'', 0)", + key_id(&federation_id) + ); + glue_db + .execute(stmt) + .await + .map_err(|_| MutinyError::ReadError { + source: MutinyStorageError::IndexedDBError, + })?; + } else if let Some(row) = rows.first() { + if let Value::Bytea(binary_data) = &row[0] { + if !binary_data.is_empty() { + let key_value_pairs: Vec<(Vec, Vec)> = + bincode::deserialize(binary_data).map_err(|e| { + MutinyError::ReadError { + source: MutinyStorageError::Other(e.into()), + } + })?; + + let mut mem_db_tx = fedimint_memory.begin_transaction().await; + for (key, value) in key_value_pairs { + mem_db_tx + .raw_insert_bytes(&key, &value) + .await + .map_err(|_| { + MutinyError::write_err(MutinyStorageError::IndexedDBError) + })?; + } + mem_db_tx.commit_tx().await.map_err(|_| { + MutinyError::write_err(MutinyStorageError::IndexedDBError) + })?; + } + } else { + return Err(MutinyError::ReadError { + source: MutinyStorageError::IndexedDBError, + }); + } + } + } else { + return Err(MutinyError::ReadError { + source: MutinyStorageError::IndexedDBError, + }); + } + } + + log_debug!(logger, "done setting up FedimintDB for fedimint"); + + Ok(Self { + db, + federation_id, + fedimint_memory: Arc::new(fedimint_memory), + }) + } +} + +fn key_id(federation_id: &str) -> String { + format!("fedimint_key_{}", federation_id) +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl IRawDatabase for FedimintDB { + type Transaction<'a> = GluePseudoTransaction<'a>; + + async fn begin_transaction<'a>(&'a self) -> GluePseudoTransaction { + GluePseudoTransaction { + db: self.db.clone(), + federation_id: self.federation_id.clone(), + mem: self.fedimint_memory.begin_transaction().await, + } + } +} + +#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] +pub struct GluePseudoTransaction<'a> { + #[cfg(not(target_arch = "wasm32"))] + pub(crate) db: Arc>>, + #[cfg(target_arch = "wasm32")] + pub(crate) db: Arc>>, + federation_id: String, + mem: MemTransaction<'a>, +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl<'a> IRawDatabaseTransaction for GluePseudoTransaction<'a> { + async fn commit_tx(mut self) -> anyhow::Result<()> { + #[cfg(not(target_arch = "wasm32"))] + unimplemented!("can't run on servers until Send is supported in Glue"); + + #[cfg(target_arch = "wasm32")] + { + let key_value_pairs = self + .mem + .raw_find_by_prefix(&[]) + .await? + .collect::, Vec)>>() + .await; + self.mem.commit_tx().await?; + + let serialized_data = + bincode::serialize(&key_value_pairs).map_err(anyhow::Error::new)?; + let hex_serialized_data = hex::encode(serialized_data); + + let update_query = format!( + "UPDATE mutiny_kv SET val = X'{}' WHERE key = '{}'", + hex_serialized_data, + key_id(&self.federation_id) + ); + + let mut db = self.db.lock().await; + + db.execute(&update_query) + .await + .map_err(anyhow::Error::new)?; + + Ok(()) + } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl<'a> IDatabaseTransactionOpsCore for GluePseudoTransaction<'a> { + async fn raw_insert_bytes( + &mut self, + key: &[u8], + value: &[u8], + ) -> anyhow::Result>> { + self.mem.raw_insert_bytes(key, value).await + } + + async fn raw_get_bytes(&mut self, key: &[u8]) -> anyhow::Result>> { + self.mem.raw_get_bytes(key).await + } + + async fn raw_remove_entry(&mut self, key: &[u8]) -> anyhow::Result>> { + self.mem.raw_remove_entry(key).await + } + + async fn raw_find_by_prefix(&mut self, key_prefix: &[u8]) -> anyhow::Result> { + self.mem.raw_find_by_prefix(key_prefix).await + } + + async fn raw_remove_by_prefix(&mut self, key_prefix: &[u8]) -> anyhow::Result<()> { + self.mem.raw_remove_by_prefix(key_prefix).await + } + + async fn raw_find_by_prefix_sorted_descending( + &mut self, + key_prefix: &[u8], + ) -> anyhow::Result> { + self.mem + .raw_find_by_prefix_sorted_descending(key_prefix) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl<'a> IDatabaseTransactionOps for GluePseudoTransaction<'a> { + async fn rollback_tx_to_savepoint(&mut self) -> anyhow::Result<()> { + self.mem.rollback_tx_to_savepoint().await + } + + async fn set_tx_savepoint(&mut self) -> anyhow::Result<()> { + self.mem.set_tx_savepoint().await + } +} + +#[cfg(test)] +use gluesql::core::store::GStore; +#[cfg(test)] +use gluesql::core::store::GStoreMut; + +#[cfg(test)] +async fn create_glue_and_fedimint_storage() { + let db = GlueDB::new( + #[cfg(target_arch = "wasm32")] + Some("create_glue_and_fedimint_storage".to_string()), + Arc::new(MutinyLogger::default()), + ) + .await + .unwrap(); + + db.new_fedimint_client_db("create_glue_storage".to_string()) + .await + .unwrap(); +} + +#[cfg(test)] +async fn create_glue_storage_value() { + let db = GlueDB::new( + #[cfg(target_arch = "wasm32")] + Some("create_glue_storage_value".to_string()), + Arc::new(MutinyLogger::default()), + ) + .await + .unwrap(); + + let f = db + .new_fedimint_client_db("create_glue_storage".to_string()) + .await + .unwrap(); + + let mut tx = f.begin_transaction().await; + let previous_bytes = tx + .raw_insert_bytes("k".as_bytes(), "v".as_bytes()) + .await + .unwrap(); + assert!(previous_bytes.is_none()); + + let tx_get_bytes = tx.raw_get_bytes("k".as_bytes()).await.unwrap(); + assert_eq!(tx_get_bytes, Some("v".as_bytes().to_vec())); + + tx.commit_tx().await.unwrap(); + + let mut tx_after_commit = f.begin_transaction().await; + let bytes_after_commit = tx_after_commit.raw_get_bytes("k".as_bytes()).await.unwrap(); + assert_eq!(bytes_after_commit, Some("v".as_bytes().to_vec())); + + // now reinit the DB and see if it loads the same values from the table + let same_f = db + .new_fedimint_client_db("create_glue_storage".to_string()) + .await + .unwrap(); + let mut tx = same_f.begin_transaction().await; + let tx_get_bytes = tx.raw_get_bytes("k".as_bytes()).await.unwrap(); + assert_eq!(tx_get_bytes, Some("v".as_bytes().to_vec())); +} + +#[cfg(test)] +async fn run_basic_glue_tests(glue: &mut Glue) { + use std::borrow::Cow; + + use gluesql::core::{ + ast::{AstLiteral, Expr}, + ast_builder::{num, table, Execute, ExprNode}, + }; + + glue.execute("DROP TABLE IF EXISTS api_test").await.unwrap(); + glue.execute( + "CREATE TABLE api_test ( + id INTEGER, + name TEXT, + nullable TEXT NULL, + is BOOLEAN + )", + ) + .await + .unwrap(); + + glue.execute( + "INSERT INTO api_test ( + id, + name, + nullable, + is + ) VALUES + (1, 'test1', 'not null', TRUE), + (2, 'test2', NULL, FALSE)", + ) + .await + .unwrap(); + + let select_query = "SELECT id, name, nullable, is FROM api_test"; + let mut result = glue.execute(select_query).await.unwrap(); + + assert_eq!(result.len(), 1); + + if let Payload::Select { rows, .. } = result.pop().unwrap() { + assert_eq!(rows.len(), 2); + + let row1 = &rows[0]; + assert_eq!(row1[0], Value::I64(1)); + assert_eq!(row1[1], Value::Str("test1".to_string())); + assert_eq!(row1[2], Value::Str("not null".to_string())); + assert_eq!(row1[3], Value::Bool(true)); + + let row2 = &rows[1]; + assert_eq!(row2[0], Value::I64(2)); + assert_eq!(row2[1], Value::Str("test2".to_string())); + assert_eq!(row2[2], Value::Null); + assert_eq!(row2[3], Value::Bool(false)); + } else { + panic!("Expected Payload::Select"); + } + + // Use AST Builder to insert another row + table("api_test") + .insert() + .columns("id, name, nullable, is") + .values(vec![vec![ + num(3), + ExprNode::QuotedString(Cow::Owned("test3".to_string())), + ExprNode::Expr(Cow::Owned(Expr::Literal(AstLiteral::Null))), + ExprNode::Expr(Cow::Owned(Expr::Literal(AstLiteral::Boolean(true)))), + ]]) + .execute(glue) + .await + .unwrap(); + + // Use AST Builder to select all rows + let ast_select_all = table("api_test").select().execute(glue).await.unwrap(); + + if let Payload::Select { rows, .. } = ast_select_all { + assert_eq!(rows.len(), 3); + + struct ApiTestRow { + id: i64, + name: String, + nullable: Option, + is: bool, + } + + let api_test_rows = rows + .into_iter() + .map(|row| ApiTestRow { + id: match row[0] { + Value::I64(val) => val, + _ => panic!("Expected I64 for id"), + }, + name: match &row[1] { + Value::Str(val) => val.clone(), + _ => panic!("Expected Str for name"), + }, + nullable: match &row[2] { + Value::Str(val) => Some(val.clone()), + Value::Null => None, + _ => panic!("Expected Str or Null for nullable"), + }, + is: match row[3] { + Value::Bool(val) => val, + _ => panic!("Expected Bool for is"), + }, + }) + .collect::>(); + + assert_eq!(api_test_rows[2].id, 3); + assert_eq!(api_test_rows[2].name, "test3"); + assert!(api_test_rows[2].nullable.is_none()); + assert!(api_test_rows[2].is); + } else { + panic!("Expected Payload::Select"); + } + + // do some bytea tests + glue.execute("CREATE TABLE IF NOT EXISTS mutiny_kv (key TEXT PRIMARY KEY, val BYTEA NOT NULL)") + .await + .unwrap(); + + let key_value_pairs = vec![ + (vec![1, 2, 3], vec![4, 5, 6]), + (vec![7, 8, 9], vec![10, 11, 12]), + ]; + let serialized_data = bincode::serialize(&key_value_pairs).unwrap(); + let hex_serialized_data = hex::encode(serialized_data); + + glue.execute("INSERT INTO mutiny_kv (key, val) VALUES ('storage', X'')") + .await + .unwrap(); + let select_query = "SELECT val FROM mutiny_kv WHERE key = 'storage'"; + let mut result = glue.execute(select_query).await.unwrap(); + + if let Payload::Select { rows, .. } = result.pop().expect("should get something") { + if let Some(row) = rows.first() { + if let Value::Bytea(hex_string) = &row[0] { + let serialized_data = hex::decode(hex_string).unwrap(); + assert!(serialized_data.is_empty()); + } else { + panic!("Expected bytea"); + } + } + } else { + panic!("Expected Payload::Select"); + } + + let update_query = format!( + "UPDATE mutiny_kv SET val = X'{}' WHERE key = 'fedimint_storage'", + hex_serialized_data + ); + glue.execute(&update_query).await.unwrap(); + + let select_query = "SELECT val FROM mutiny_kv WHERE key = 'fedimint_storage'"; + let mut result = glue.execute(select_query).await.unwrap(); + + if let Payload::Select { rows, .. } = result.pop().expect("should get something") { + if let Some(row) = rows.first() { + if let Value::Bytea(binary_data) = &row[0] { + let retrieved_pairs: Vec<(Vec, Vec)> = + bincode::deserialize(binary_data).unwrap(); + + assert_eq!(retrieved_pairs, key_value_pairs); + } else { + panic!("Expected bytea"); + } + } + } else { + panic!("Expected Payload::Select"); + } +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use super::*; + use gluesql::prelude::MemoryStorage; + use tokio; + + #[tokio::test] + async fn basic_glue_tests() { + let storage = MemoryStorage::default(); + let mut glue = Glue::new(storage); + run_basic_glue_tests(&mut glue).await; + } + + #[cfg(feature = "ignored_tests")] + #[tokio::test] + async fn create_glue_storage_tests() { + create_glue_and_fedimint_storage().await; + } + + #[cfg(feature = "ignored_tests")] + #[tokio::test] + async fn create_glue_storage_value_tests() { + create_glue_storage_value().await; + } +} + +#[cfg(test)] +#[cfg(target_arch = "wasm32")] +mod wasm_tests { + use gluesql::prelude::{Glue, IdbStorage}; + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + use super::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + async fn basic_wasm32_glue_tests() { + let storage = IdbStorage::new(Some("basic_wasm32_glue_tests".to_string())) + .await + .unwrap(); + let mut glue = Glue::new(storage); + run_basic_glue_tests(&mut glue).await; + } + + #[test] + async fn create_glue_storage_tests() { + create_glue_and_fedimint_storage().await; + } + + #[test] + async fn create_glue_storage_value_tests() { + create_glue_storage_value().await; + } +} diff --git a/mutiny-core/src/sql/mod.rs b/mutiny-core/src/sql/mod.rs new file mode 100644 index 000000000..a55d14889 --- /dev/null +++ b/mutiny-core/src/sql/mod.rs @@ -0,0 +1 @@ +pub mod glue; From f4ae060bf643ebec61a0dcd0ec0c08963353ee47 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Tue, 19 Dec 2023 13:44:06 -0600 Subject: [PATCH 08/12] add metadata to federation identities --- mutiny-core/src/federation.rs | 11 ++++++++++- mutiny-core/src/lib.rs | 34 ++++++++++++++++++++++++++-------- mutiny-wasm/src/lib.rs | 4 +++- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index d69fa353a..fb1063627 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -116,6 +116,10 @@ pub struct FederationStorage { pub struct FederationIdentity { pub uuid: String, pub federation_id: FederationId, + // https://github.com/fedimint/fedimint/tree/master/docs/meta_fields + pub federation_name: Option, + pub federation_expiry_timestamp: Option, + pub welcome_message: Option, } // This is the FederationIndex reference that is saved to the DB @@ -356,10 +360,15 @@ impl FederationClient { } } - pub async fn get_mutiny_federation_identity(&self) -> FederationIdentity { + pub fn get_mutiny_federation_identity(&self) -> FederationIdentity { FederationIdentity { uuid: self.uuid.clone(), federation_id: self.fedimint_client.federation_id(), + federation_name: self.fedimint_client.get_meta("federation_name"), + federation_expiry_timestamp: self + .fedimint_client + .get_meta("federation_expiry_timestamp"), + welcome_message: self.fedimint_client.get_meta("welcome_message"), } } } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index aa8129090..290a2bf7b 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -564,7 +564,7 @@ impl MutinyWallet { .ok_or(MutinyError::InvoiceInvalid)?; // Try each federation first - let federation_ids = self.list_federations().await?; + let federation_ids = self.list_federation_ids().await?; for federation_id in federation_ids { if let Some(fedimint_client) = self.federations.lock().await.get(&federation_id) { // Check if the federation has enough balance @@ -635,7 +635,7 @@ impl MutinyWallet { None } else { // Check if a federation exists - let federation_ids = self.list_federations().await?; + let federation_ids = self.list_federation_ids().await?; if !federation_ids.is_empty() { // Use the first federation for simplicity let federation_id = &federation_ids[0]; @@ -1032,13 +1032,23 @@ impl MutinyWallet { } /// Lists the federation id's of the federation clients in the manager. - pub async fn list_federations(&self) -> Result, MutinyError> { + pub async fn list_federations(&self) -> Result, MutinyError> { let federations = self.federations.lock().await; - let federation_ids = federations + let federation_identities = federations + .iter() + .map(|(_, n)| n.get_mutiny_federation_identity()) + .collect(); + Ok(federation_identities) + } + + /// Lists the federation id's of the federation clients in the manager. + pub async fn list_federation_ids(&self) -> Result, MutinyError> { + let federations = self.federations.lock().await; + let federation_identities = federations .iter() .map(|(_, n)| n.fedimint_client.federation_id()) .collect(); - Ok(federation_ids) + Ok(federation_identities) } /// Removes a federation by setting its archived status to true, based on the FederationId. @@ -1066,7 +1076,7 @@ impl MutinyWallet { } pub async fn get_total_federation_balance(&self) -> Result { - let federation_ids = self.list_federations().await?; + let federation_ids = self.list_federation_ids().await?; let mut total_balance = 0; let federations = self.federations.lock().await; @@ -1086,13 +1096,13 @@ impl MutinyWallet { pub async fn get_federation_balances(&self) -> Result { let federation_lock = self.federations.lock().await; - let federation_ids = self.list_federations().await?; + let federation_ids = self.list_federation_ids().await?; let mut balances = Vec::with_capacity(federation_ids.len()); for fed_id in federation_ids { let fedimint_client = federation_lock.get(&fed_id).ok_or(MutinyError::NotFound)?; let balance = fedimint_client.get_balance().await?; - let identity = fedimint_client.get_mutiny_federation_identity().await; + let identity = fedimint_client.get_mutiny_federation_identity(); balances.push(FederationBalance { identity, balance }); } @@ -1198,6 +1208,11 @@ pub(crate) async fn create_new_federation( .await?; let federation_id = new_federation.fedimint_client.federation_id(); + let federation_name = new_federation.fedimint_client.get_meta("federation_name"); + let federation_expiry_timestamp = new_federation + .fedimint_client + .get_meta("federation_expiry_timestamp"); + let welcome_message = new_federation.fedimint_client.get_meta("welcome_message"); federations .lock() .await @@ -1206,6 +1221,9 @@ pub(crate) async fn create_new_federation( Ok(FederationIdentity { uuid: next_federation_uuid.clone(), federation_id, + federation_name, + federation_expiry_timestamp, + welcome_message, }) } diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index af400c405..6dd017187 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -1022,7 +1022,9 @@ impl MutinyWallet { /// Lists the federation id's of the federation clients in the manager. #[wasm_bindgen] - pub async fn list_federations(&self) -> Result */, MutinyJsError> { + pub async fn list_federations( + &self, + ) -> Result */, MutinyJsError> { Ok(JsValue::from_serde(&self.inner.list_federations().await?)?) } From 84f934b6c3b56b65926fd2ccc9a44d6142698ac7 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Tue, 19 Dec 2023 13:35:57 -0600 Subject: [PATCH 09/12] Add payments to gluedb --- Cargo.lock | 1 + mutiny-core/Cargo.toml | 1 + mutiny-core/src/event.rs | 15 + mutiny-core/src/federation.rs | 305 +++++++++++------ mutiny-core/src/lib.rs | 9 +- mutiny-core/src/sql/glue.rs | 616 +++++++++++++++++++++++++++++++++- mutiny-core/src/sql/mod.rs | 26 ++ 7 files changed, 866 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5383b9f3..13ab3b9b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2468,6 +2468,7 @@ dependencies = [ "getrandom", "gloo-net", "gluesql", + "gluesql-core", "hex", "instant", "itertools 0.11.0", diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index 5e6cf5bd7..c071644ae 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -46,6 +46,7 @@ jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] } argon2 = { version = "0.5.0", features = ["password-hash", "alloc"] } payjoin = { version = "0.10.0", features = ["send", "base64"] } gluesql = { version = "0.15", default-features = false, features = ["memory-storage"] } +gluesql-core = "0.15.0" bincode = "1.3.3" hex = "0.4.3" diff --git a/mutiny-core/src/event.rs b/mutiny-core/src/event.rs index 87ee94e92..997df541b 100644 --- a/mutiny-core/src/event.rs +++ b/mutiny-core/src/event.rs @@ -22,6 +22,7 @@ use lightning::{ }; use lightning_invoice::Bolt11Invoice; use serde::{Deserialize, Serialize}; +use std::str::FromStr; use std::sync::Arc; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] @@ -78,6 +79,20 @@ impl fmt::Display for HTLCStatus { } } +impl FromStr for HTLCStatus { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "Pending" => Ok(HTLCStatus::Pending), + "InFlight" => Ok(HTLCStatus::InFlight), + "Succeeded" => Ok(HTLCStatus::Succeeded), + "Failed" => Ok(HTLCStatus::Failed), + _ => Err(format!("'{}' is not a valid HTLCStatus", s)), + } + } +} + #[derive(Clone)] pub struct EventHandler { channel_manager: Arc>, diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index fb1063627..4fd9b4aa6 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -4,9 +4,9 @@ use crate::{ logging::MutinyLogger, nodemanager::MutinyInvoice, onchain::coin_type_from_network, - sql::glue::GlueDB, + sql::{glue::GlueDB, ApplicationStore}, storage::MutinyStorage, - utils::sleep, + utils::{self, sleep}, ActivityItem, HTLCStatus, DEFAULT_PAYMENT_TIMEOUT, }; use bip39::Mnemonic; @@ -19,6 +19,7 @@ use bitcoin::{ }; use fedimint_bip39::Bip39RootSecretStrategy; use fedimint_client::{ + db::ChronologicalOperationLogKey, derivable_secret::DerivableSecret, get_config_from_db, oplog::{OperationLogEntry, UpdateStreamOrOutcome}, @@ -41,8 +42,8 @@ use fedimint_ln_common::LightningCommonInit; use fedimint_mint_client::MintClientInit; use fedimint_wallet_client::WalletClientInit; use futures::future::{self}; -use futures_util::{pin_mut, stream, StreamExt}; -use lightning::{log_debug, log_info, log_warn, util::logger::Logger}; +use futures_util::{pin_mut, StreamExt}; +use lightning::{log_debug, log_error, log_info, log_trace, log_warn, util::logger::Logger}; use lightning_invoice::Bolt11Invoice; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ @@ -141,6 +142,7 @@ pub(crate) struct FederationClient { pub(crate) fedimint_client: ClientArc, stopped_components: Arc>>, storage: S, + g: GlueDB, network: Network, pub(crate) logger: Arc, stop: Arc, @@ -204,19 +206,34 @@ impl FederationClient { fedimint_client, stopped_components, storage, + g, network, logger, stop, }) } - pub(crate) async fn get_invoice(&self, amount: u64) -> Result { + pub(crate) async fn get_invoice( + &self, + amount: u64, + labels: Vec, + ) -> Result { let lightning_module = self .fedimint_client .get_first_module::(); let (_id, invoice) = lightning_module .create_bolt11_invoice(Amount::from_sats(amount), String::new(), None, ()) .await?; + + // persist the invoice + let mut stored_payment: MutinyInvoice = invoice.clone().into(); + stored_payment.inbound = true; + stored_payment.labels = labels; + + log_trace!(self.logger, "Persiting payment"); + self.g.save_payment(stored_payment).await?; + log_trace!(self.logger, "Persisted payment"); + Ok(invoice.into()) } @@ -226,92 +243,215 @@ impl FederationClient { } pub async fn get_activity(&self) -> Result, MutinyError> { - let operations = self - .fedimint_client - .operation_log() - .list_operations(FEDIMINT_OPERATIONS_LIST_MAX, None) - .await; + log_trace!(self.logger, "Getting activity"); + let payments = self.g.list_payments().await?; + + let mut payments_map: HashMap = HashMap::new(); + let mut pending_invoices: Vec<&MutinyInvoice> = Vec::new(); + + for payment in payments.iter() { + payments_map.insert(payment.payment_hash, payment.clone()); + if matches!(payment.status, HTLCStatus::InFlight | HTLCStatus::Pending) { + pending_invoices.push(payment); + } + } + + let operations = if !pending_invoices.is_empty() { + log_trace!(self.logger, "pending invoices, going to list operations"); + self.fedimint_client + .operation_log() + .list_operations(FEDIMINT_OPERATIONS_LIST_MAX, None) + .await + } else { + vec![] + }; let lightning_module = Arc::new( self.fedimint_client .get_first_module::(), ); - let activity_stream = stream::iter(operations.into_iter()); - - let activity_items = activity_stream - .then(move |(key, entry)| { - let logger = self.logger.clone(); - let lightning_module = Arc::clone(&lightning_module); - async move { - if entry.operation_module_kind() == LightningCommonInit::KIND.as_str() { - Some( - handle_ln_meta(logger, &entry, key.operation_id, &lightning_module) - .await, - ) - } else { - log_warn!( - logger, - "Unsupported module: {}", - entry.operation_module_kind() - ); - None + let mut operation_map: HashMap< + sha256::Hash, + (ChronologicalOperationLogKey, OperationLogEntry), + > = HashMap::new(); + log_trace!( + self.logger, + "About to go through {} operations", + operations.len() + ); + for (key, entry) in operations { + if entry.operation_module_kind() == LightningCommonInit::KIND.as_str() { + let lightning_meta: LightningOperationMeta = entry.meta(); + match lightning_meta.variant { + LightningOperationMetaVariant::Pay(pay_meta) => { + operation_map.insert(*pay_meta.invoice.payment_hash(), (key, entry)); } + LightningOperationMetaVariant::Receive { invoice, .. } => { + operation_map.insert(*invoice.payment_hash(), (key, entry)); + } + } + } + } + + log_trace!( + self.logger, + "Going through {} pending invoices to extract status", + pending_invoices.len() + ); + for invoice in pending_invoices { + let hash = invoice.payment_hash; + if let Some((key, entry)) = operation_map.get(&hash) { + if let Some(updated_invoice) = extract_invoice_from_entry( + self.logger.clone(), + entry, + &hash, + key.operation_id, + &lightning_module, + ) + .await + { + self.maybe_update_after_checking_fedimint(updated_invoice.clone()) + .await?; + payments_map.insert(hash, updated_invoice); + } + } + } + + let updated_payments = payments_map.into_values().collect::>(); + + let activity_items = updated_payments + .into_iter() + .filter_map(|invoice| { + if !invoice + .bolt11 + .as_ref() + .is_some_and(|b| b.would_expire(utils::now())) + && matches!(invoice.status, HTLCStatus::Succeeded | HTLCStatus::InFlight) + { + Some(ActivityItem::Lightning(Box::new(invoice))) + } else { + None } }) - .filter_map(|item| async move { item }) - .collect::>() - .await; + .collect::>(); Ok(activity_items) } + async fn maybe_update_after_checking_fedimint( + &self, + updated_invoice: MutinyInvoice, + ) -> Result<(), MutinyError> { + if matches!( + updated_invoice.status, + HTLCStatus::Succeeded | HTLCStatus::Failed + ) { + log_debug!(self.logger, "Saving updated payment"); + self.g + .update_payment_status( + &updated_invoice.payment_hash, + updated_invoice.status.clone(), + ) + .await?; + self.g + .update_payment_fee(&updated_invoice.payment_hash, updated_invoice.fees_paid) + .await?; + self.g + .update_payment_preimage( + &updated_invoice.payment_hash, + updated_invoice.preimage.clone(), + ) + .await?; + } + Ok(()) + } + pub async fn get_invoice_by_hash( &self, hash: &sha256::Hash, ) -> Result { - let operations = self - .fedimint_client - .operation_log() - .list_operations(FEDIMINT_OPERATIONS_LIST_MAX, None) - .await; + log_trace!(self.logger, "get_invoice_by_hash"); + + // Try to get the invoice from storage first + let invoice = match self.g.get_payment(hash).await { + Ok(i) => i, + Err(e) => { + log_error!(self.logger, "could not get invoice by hash: {e}"); + return Err(e); + } + }; - let lightning_module = self - .fedimint_client - .get_first_module::(); + if let Some(invoice) = invoice { + log_trace!(self.logger, "retrieved invoice by hash"); - for (key, entry) in operations { - if entry.operation_module_kind() == LightningCommonInit::KIND.as_str() { - if let Some(invoice) = extract_invoice_from_entry( - self.logger.clone(), - &entry, - hash, - key.operation_id, - &lightning_module, - ) - .await - { - return Ok(invoice); - } - } else { - log_warn!( + if matches!(invoice.status, HTLCStatus::InFlight | HTLCStatus::Pending) { + log_trace!(self.logger, "invoice still in flight, getting operations"); + // If the invoice is InFlight or Pending, check the operation log for updates + let lightning_module = self + .fedimint_client + .get_first_module::(); + + let operations = self + .fedimint_client + .operation_log() + .list_operations(FEDIMINT_OPERATIONS_LIST_MAX, None) + .await; + + log_trace!( self.logger, - "Unsupported module: {}", - entry.operation_module_kind() + "going to go through {} operations", + operations.len() ); + for (key, entry) in operations { + if entry.operation_module_kind() == LightningCommonInit::KIND.as_str() { + if let Some(updated_invoice) = extract_invoice_from_entry( + self.logger.clone(), + &entry, + hash, + key.operation_id, + &lightning_module, + ) + .await + { + self.maybe_update_after_checking_fedimint(updated_invoice.clone()) + .await?; + return Ok(updated_invoice); + } + } else { + log_warn!( + self.logger, + "Unsupported module: {}", + entry.operation_module_kind() + ); + } + } + } else { + // If the invoice is not InFlight or Pending, return it directly + log_trace!(self.logger, "returning final invoice"); + return Ok(invoice); } } + log_debug!(self.logger, "could not find invoice"); Err(MutinyError::NotFound) } pub(crate) async fn pay_invoice( &self, invoice: Bolt11Invoice, + labels: Vec, ) -> Result { + // Save before sending + let mut stored_payment: MutinyInvoice = invoice.clone().into(); + stored_payment.inbound = false; + stored_payment.labels = labels; + self.g.save_payment(stored_payment.clone()).await?; + let lightning_module = self .fedimint_client .get_first_module::(); + let outgoing_payment = lightning_module .pay_bolt11_invoice(invoice.clone(), ()) .await?; @@ -352,6 +492,9 @@ impl FederationClient { } }; + self.maybe_update_after_checking_fedimint(inv.clone()) + .await?; + match inv.status { HTLCStatus::Succeeded => Ok(inv), HTLCStatus::Failed => Err(MutinyError::RoutingFailed), @@ -456,52 +599,6 @@ async fn extract_invoice_from_entry( } } -async fn handle_ln_meta( - logger: Arc, - entry: &OperationLogEntry, - operation_id: OperationId, - lightning_module: &LightningClientModule, -) -> ActivityItem { - let lightning_meta: LightningOperationMeta = entry.meta(); - - let invoice = match lightning_meta.variant { - LightningOperationMetaVariant::Pay(pay_meta) => { - match lightning_module.subscribe_ln_pay(operation_id).await { - Ok(o) => { - process_outcome( - o, - process_pay_state_ln, - pay_meta.invoice, - false, - FEDIMINT_STATUS_TIMEOUT_CHECK_MS, - logger, - ) - .await - } - Err(_) => pay_meta.invoice.into(), - } - } - LightningOperationMetaVariant::Receive { invoice, .. } => { - match lightning_module.subscribe_ln_receive(operation_id).await { - Ok(o) => { - process_outcome( - o, - process_receive_state, - invoice, - true, - FEDIMINT_STATUS_TIMEOUT_CHECK_MS, - logger, - ) - .await - } - Err(_) => invoice.into(), - } - } - }; - - ActivityItem::Lightning(Box::new(invoice)) -} - fn process_pay_state_internal(pay_state: InternalPayState) -> (HTLCStatus, Option) { let status: HTLCStatus = pay_state.clone().into(); @@ -556,7 +653,7 @@ where match stream_or_outcome { UpdateStreamOrOutcome::Outcome(outcome) => { invoice.status = outcome.into(); - log_debug!(logger, "Outcome received: {}", invoice.status); + log_trace!(logger, "Outcome received: {}", invoice.status); } UpdateStreamOrOutcome::UpdateStream(mut s) => { let timeout_future = sleep(timeout as i32); diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 290a2bf7b..8f1658087 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -571,7 +571,9 @@ impl MutinyWallet { let balance = fedimint_client.get_balance().await?; if balance >= send_msat / 1_000 { // Try to pay the invoice using the federation - let payment_result = fedimint_client.pay_invoice(inv.clone()).await; + let payment_result = fedimint_client + .pay_invoice(inv.clone(), labels.clone()) + .await; match payment_result { Ok(r) => return Ok(r), Err(e) => match e { @@ -644,7 +646,10 @@ impl MutinyWallet { match fedimint_client { Some(client) => { // Try to create an invoice using the federation - match client.get_invoice(amount.unwrap_or_default()).await { + match client + .get_invoice(amount.unwrap_or_default(), labels.clone()) + .await + { Ok(inv) => Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?), Err(_) => None, // Handle the error or fallback to node_manager invoice creation } diff --git a/mutiny-core/src/sql/glue.rs b/mutiny-core/src/sql/glue.rs index ce109037f..f42ed9167 100644 --- a/mutiny-core/src/sql/glue.rs +++ b/mutiny-core/src/sql/glue.rs @@ -1,21 +1,34 @@ +use crate::HTLCStatus; use crate::{ error::{MutinyError, MutinyStorageError}, logging::MutinyLogger, + nodemanager::MutinyInvoice, + sql::ApplicationStore, }; use async_trait::async_trait; +use bitcoin::hashes::sha256; +use bitcoin::secp256k1::PublicKey; use fedimint_core::db::{ mem_impl::{MemDatabase, MemTransaction}, IDatabaseTransactionOps, IDatabaseTransactionOpsCore, IRawDatabase, IRawDatabaseTransaction, PrefixStream, }; use gluesql::prelude::{Glue, Payload, Value}; -use lightning::{log_debug, util::logger::Logger}; +use lightning::{log_debug, log_error, log_trace, util::logger::Logger}; +use lightning_invoice::Bolt11Invoice; +use std::str::FromStr; use std::{fmt, sync::Arc}; #[cfg(not(target_arch = "wasm32"))] use tokio::sync::Mutex; +#[cfg(target_arch = "wasm32")] +use gluesql::core::executor::ValidateError; + +#[cfg(target_arch = "wasm32")] +use crate::utils; + #[cfg(target_arch = "wasm32")] use futures::lock::Mutex; @@ -75,6 +88,26 @@ impl GlueDB { .await .map_err(|_| MutinyError::write_err(MutinyStorageError::IndexedDBError))?; + glue_db + .execute( + "CREATE TABLE IF NOT EXISTS mutiny_invoice ( + bolt11 TEXT, + description TEXT NULL, + payment_hash TEXT PRIMARY KEY, + preimage TEXT NULL, + payee_pubkey TEXT NULL, + amount_sats INTEGER NULL, + expire INTEGER NOT NULL, + status TEXT NOT NULL, + fees_paid INTEGER NULL, + inbound BOOLEAN, + labels TEXT, + last_updated INTEGER + )", + ) + .await + .map_err(|_| MutinyError::write_err(MutinyStorageError::IndexedDBError))?; + log_debug!(logger, "done setting up GlueDB"); Ok(Self { @@ -91,6 +124,364 @@ impl GlueDB { } } +impl ApplicationStore for GlueDB { + #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] + async fn save_payment(&self, invoice: MutinyInvoice) -> Result<(), MutinyError> { + #[cfg(not(target_arch = "wasm32"))] + unimplemented!("can't run on servers until Send is supported in Glue"); + + #[cfg(target_arch = "wasm32")] + { + let labels_json = serde_json::to_string(&invoice.labels).map_err(|_| { + MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + } + })?; + let payment_hash = invoice.payment_hash.to_string(); + let payee_pubkey = invoice.payee_pubkey.map(|k| k.to_string()); + let bolt11 = invoice.bolt11.map(|b| b.to_string()); + let preimage = invoice.preimage.clone(); + let status = invoice.status.to_string(); + + let sql = format!( + "INSERT INTO mutiny_invoice (bolt11, description, payment_hash, preimage, payee_pubkey, amount_sats, expire, status, fees_paid, inbound, labels, last_updated) + VALUES ({}, {}, '{}', {}, {}, {}, {}, '{}', {}, {}, '{}', {})", + bolt11.as_ref().map(|s| format!("'{}'", s)).unwrap_or("NULL".to_string()), + invoice.description.as_ref().map(|s| format!("'{}'", s)).unwrap_or("NULL".to_string()), + payment_hash, + preimage.as_ref().map(|s| format!("'{}'", s)).unwrap_or("NULL".to_string()), + payee_pubkey.as_ref().map(|s| format!("'{}'", s)).unwrap_or("NULL".to_string()), + invoice.amount_sats.as_ref().map(|s| format!("{}", s)).unwrap_or("NULL".to_string()), + invoice.expire, + status, + invoice.fees_paid.as_ref().map(|s| format!("{}", s)).unwrap_or("NULL".to_string()), + invoice.inbound, + labels_json, + invoice.last_updated, + ); + + let mut glue = self.db.lock().await; + match glue.execute(&sql).await { + Ok(_) => Ok(()), + Err(gluesql_core::error::Error::Validate( + ValidateError::DuplicateEntryOnPrimaryKeyField(_), + )) => { + // Define the UPDATE query + let update_sql = format!( + "UPDATE mutiny_invoice + SET bolt11 = {}, description = {}, preimage = {}, payee_pubkey = {}, + amount_sats = {}, expire = {}, status = '{}', fees_paid = {}, inbound = {}, + labels = '{}', last_updated = {} + WHERE payment_hash = '{}'", + bolt11.as_ref().map(|s| format!("'{}'", s)).unwrap_or("NULL".to_string()), + invoice.description.as_ref().map(|s| format!("'{}'", s)).unwrap_or("NULL".to_string()), + preimage.as_ref().map(|s| format!("'{}'", s)).unwrap_or("NULL".to_string()), + payee_pubkey.as_ref().map(|s| format!("'{}'", s)).unwrap_or("NULL".to_string()), + invoice.amount_sats.as_ref().map(|s| format!("{}", s)).unwrap_or("NULL".to_string()), + invoice.expire, + status, + invoice.fees_paid.as_ref().map(|s| format!("{}", s)).unwrap_or("NULL".to_string()), + invoice.inbound, + labels_json, + invoice.last_updated, + payment_hash, + ); + + glue.execute(&update_sql).await.map_err(|_| { + MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + } + })?; + Ok(()) + } + _ => Err(MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + }), + } + } + } + + #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] + async fn get_payment( + &self, + payment_hash: &bitcoin::hashes::sha256::Hash, + ) -> Result, MutinyError> { + #[cfg(not(target_arch = "wasm32"))] + unimplemented!("can't run on servers until Send is supported in Glue"); + + #[cfg(target_arch = "wasm32")] + { + log_trace!(self.logger, "calling get_payment"); + + let payment_hash_str = payment_hash.to_string(); + let select_query = format!( + "SELECT bolt11, description, payment_hash, preimage, payee_pubkey, amount_sats, expire, status, fees_paid, inbound, labels, last_updated FROM mutiny_invoice WHERE payment_hash = '{}'", + payment_hash_str + ); + + log_trace!(self.logger, "locking db"); + let mut glue = self.db.lock().await; + log_trace!(self.logger, "running query: {select_query}"); + let mut result = glue.execute(&select_query).await.map_err(|e| { + log_error!( + self.logger, + "failed to execute query ({}): {e}", + select_query + ); + MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + } + })?; + + log_trace!(self.logger, "going through rows"); + if let Payload::Select { rows, .. } = result.pop().unwrap() { + if let Some(row) = rows.first() { + log_trace!(self.logger, "parsing first row"); + let invoice = parse_row_to_invoice(row.to_vec(), self.logger.clone())?; + Ok(Some(invoice)) + } else { + Ok(None) + } + } else { + log_error!( + self.logger, + "could not find a row when executing query ({})", + select_query + ); + Err(MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + }) + } + } + } + + #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] + async fn update_payment_status( + &self, + payment_hash: &bitcoin::hashes::sha256::Hash, + status: HTLCStatus, + ) -> Result<(), MutinyError> { + #[cfg(not(target_arch = "wasm32"))] + unimplemented!("can't run on servers until Send is supported in Glue"); + + #[cfg(target_arch = "wasm32")] + { + let status_str = status.to_string(); + let payment_hash_str = payment_hash.to_string(); + let now = utils::now().as_secs(); + let sql = format!( + "UPDATE mutiny_invoice SET status = '{}', last_updated = {} WHERE payment_hash = '{}'", + status_str, now, payment_hash_str + ); + + let mut glue = self.db.lock().await; + glue.execute(&sql) + .await + .map_err(|_| MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + })?; + Ok(()) + } + } + + #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] + async fn update_payment_fee( + &self, + payment_hash: &bitcoin::hashes::sha256::Hash, + fee: Option, + ) -> Result<(), MutinyError> { + #[cfg(not(target_arch = "wasm32"))] + unimplemented!("can't run on servers until Send is supported in Glue"); + + #[cfg(target_arch = "wasm32")] + { + let fee_str = match fee { + Some(fee_value) => fee_value.to_string(), + None => "NULL".to_string(), + }; + let payment_hash_str = payment_hash.to_string(); + let now = utils::now().as_secs(); + let sql = format!( + "UPDATE mutiny_invoice SET fees_paid = {}, last_updated = {} WHERE payment_hash = '{}'", + fee_str, now, payment_hash_str + ); + + let mut glue = self.db.lock().await; + glue.execute(&sql) + .await + .map_err(|_| MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + })?; + Ok(()) + } + } + + #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] + async fn update_payment_preimage( + &self, + payment_hash: &bitcoin::hashes::sha256::Hash, + preimage: Option, + ) -> Result<(), MutinyError> { + #[cfg(not(target_arch = "wasm32"))] + unimplemented!("can't run on servers until Send is supported in Glue"); + + #[cfg(target_arch = "wasm32")] + { + let preimage_str = match preimage { + Some(ref img) => format!("'{}'", img), + None => "NULL".to_string(), + }; + let payment_hash_str = payment_hash.to_string(); + let now = utils::now().as_secs(); + let sql = format!( + "UPDATE mutiny_invoice SET preimage = {}, last_updated = {} WHERE payment_hash = '{}'", + preimage_str, now, payment_hash_str + ); + + let mut glue = self.db.lock().await; + glue.execute(&sql) + .await + .map_err(|_| MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + })?; + Ok(()) + } + } + + #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] + async fn list_payments(&self) -> Result, MutinyError> { + #[cfg(not(target_arch = "wasm32"))] + unimplemented!("can't run on servers until Send is supported in Glue"); + + #[cfg(target_arch = "wasm32")] + { + let select_query = "SELECT bolt11, description, payment_hash, preimage, payee_pubkey, amount_sats, expire, status, fees_paid, inbound, labels, last_updated FROM mutiny_invoice"; + + let mut glue = self.db.lock().await; + let mut result = + glue.execute(&select_query) + .await + .map_err(|_| MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + })?; + + let mut invoices = Vec::new(); + if let Payload::Select { rows, .. } = result.pop().unwrap() { + for row in rows { + let invoice = parse_row_to_invoice(row.to_vec(), self.logger.clone())?; + invoices.push(invoice); + } + } else { + return Err(MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + }); + } + + Ok(invoices) + } + } +} + +#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] +fn parse_row_to_invoice( + row: Vec, + logger: Arc, +) -> Result { + let bolt11 = match &row[0] { + Value::Str(val) => { + let b = Bolt11Invoice::from_str(val).map_err(|e| { + log_error!(logger, "failed to parse invoice ({}): {e}", val); + e + })?; + Some(b) + } + _ => None, + }; + let description = match &row[1] { + Value::Str(val) => Some(val.clone()), + _ => None, + }; + let payment_hash = match &row[2] { + Value::Str(val) => sha256::Hash::from_str(val).map_err(|e| { + log_error!(logger, "failed to parse hash ({}): {e}", val); + e + })?, + _ => panic!("Expected String for preimage"), + }; + let preimage = match &row[3] { + Value::Str(val) => Some(val.clone()), + _ => None, + }; + let payee_pubkey = match &row[4] { + Value::Str(val) => { + let b = PublicKey::from_str(val).map_err(|e| { + log_error!(logger, "failed to parse pubkey ({}): {e}", val); + MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + } + })?; + Some(b) + } + _ => None, + }; + let amount_sats = match &row[5] { + Value::I64(val) => Some(*val as u64), + _ => None, + }; + let expire = match &row[6] { + Value::I64(val) => *val as u64, + _ => panic!("Expected i64 for expire"), + }; + let status = match &row[7] { + Value::Str(val) => HTLCStatus::from_str(val).map_err(|e| { + log_error!(logger, "failed to parse status ({}): {e}", val); + MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + } + })?, + _ => HTLCStatus::Pending, + }; + let fees_paid = match &row[8] { + Value::I64(val) => Some(*val as u64), + _ => None, + }; + let inbound = match &row[9] { + Value::Bool(val) => *val, + _ => false, + }; + let labels = match &row[10] { + Value::Str(val) => { + let labels_vec: Vec = serde_json::from_str(val).map_err(|e| { + log_error!(logger, "failed to parse labels ({}): {e}", val); + MutinyError::PersistenceFailed { + source: MutinyStorageError::IndexedDBError, + } + })?; + labels_vec + } + _ => vec![], + }; + let last_updated = match &row[11] { + Value::I64(val) => *val as u64, + _ => panic!("Expected i64 for last_updated"), + }; + + Ok(MutinyInvoice { + bolt11, + description, + payment_hash, + preimage, + payee_pubkey, + amount_sats, + expire, + status, + fees_paid, + inbound, + labels, + last_updated, + }) +} + #[cfg(target_arch = "wasm32")] #[derive(Clone)] pub struct FedimintDB { @@ -324,6 +715,207 @@ async fn create_glue_and_fedimint_storage() { .unwrap(); } +#[cfg(test)] +async fn create_glue_and_payments_storage() { + use bitcoin::hashes::hex::{FromHex, ToHex}; + + const INVOICE: &str = "lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm"; + + let db = GlueDB::new( + #[cfg(target_arch = "wasm32")] + Some("create_glue_and_payments_storage".to_string()), + Arc::new(MutinyLogger::default()), + ) + .await + .unwrap(); + + let preimage: [u8; 32] = + FromHex::from_hex("7600f5a9ad72452dea7ad86dabbc9cb46be96a1a2fcd961e041d066b38d93008") + .unwrap(); + + let payment_hash = + sha256::Hash::from_hex("55ecf9169a6fa07e8ba181fdddf5b0bcc7860176659fa22a7cca9da2a359a33b") + .unwrap(); + + let pubkey = + PublicKey::from_str("02465ed5be53d04fde66c9418ff14a5f2267723810176c9212b722e542dc1afb1b") + .unwrap(); + + let i = Bolt11Invoice::from_str(INVOICE).unwrap(); + + let label = "test".to_string(); + let labels = vec![label.clone()]; + + let invoice1: MutinyInvoice = MutinyInvoice { + bolt11: Some(i), + description: Some("dest".to_string()), + payment_hash, + preimage: Some(preimage.to_hex()), + payee_pubkey: Some(pubkey), + amount_sats: Some(100), + expire: 1681781585, + status: HTLCStatus::Succeeded, + fees_paid: Some(1), + inbound: false, + labels: labels.clone(), + last_updated: 1681781585, + }; + db.save_payment(invoice1.clone()).await.unwrap(); + + let db_invoice = db.get_payment(&payment_hash).await.unwrap().unwrap(); + assert_eq!(invoice1, db_invoice); + + let db_invoices = db.list_payments().await.unwrap(); + assert_eq!(db_invoices.len(), 1); + assert_eq!(invoice1, db_invoices[0]); + + let payment_hash2 = + sha256::Hash::from_hex("05ecf9169a6fa07e8ba181fdddf5b0bcc7860176659fa22a7cca9da2a359a33b") + .unwrap(); + + let i = Bolt11Invoice::from_str(INVOICE).unwrap(); + + let mut invoice2: MutinyInvoice = MutinyInvoice { + bolt11: Some(i.clone()), + description: Some("dest".to_string()), + payment_hash: payment_hash2, + preimage: Some(preimage.to_hex()), + payee_pubkey: Some(pubkey), + amount_sats: Some(1000), + expire: 1681781585, + status: HTLCStatus::Pending, + fees_paid: Some(1), + inbound: false, + labels: labels.clone(), + last_updated: 1681781585, + }; + db.save_payment(invoice2.clone()).await.unwrap(); + + let db_invoice2 = db.get_payment(&payment_hash2).await.unwrap().unwrap(); + assert_eq!(invoice2, db_invoice2); + assert_ne!(invoice1, db_invoice2); + + let db_invoices = db.list_payments().await.unwrap(); + assert_eq!(db_invoices.len(), 2); + assert_eq!(invoice1, db_invoices[1]); + assert_eq!(invoice2, db_invoices[0]); + + invoice2.status = HTLCStatus::Succeeded; + db.save_payment(invoice2.clone()).await.unwrap(); + let db_invoice2_updated = db.get_payment(&payment_hash2).await.unwrap().unwrap(); + assert_eq!(invoice2, db_invoice2_updated); + assert_ne!(db_invoice2, db_invoice2_updated); + + // allow nullable values + let payment_hash_nullable = + sha256::Hash::from_hex("44ecf9169a6fa07e8ba181fdddf5b0bcc7860176659fa22a7cca9da2a359a33b") + .unwrap(); + let invoice_nullable: MutinyInvoice = MutinyInvoice { + bolt11: Some(i), + description: None, + payment_hash: payment_hash_nullable, + preimage: None, + payee_pubkey: None, + amount_sats: None, + expire: 1681781585, + status: HTLCStatus::Succeeded, + fees_paid: None, + inbound: false, + labels: labels.clone(), + last_updated: 1681781585, + }; + db.save_payment(invoice_nullable.clone()).await.unwrap(); + + let db_invoice_nullable = db + .get_payment(&payment_hash_nullable) + .await + .unwrap() + .unwrap(); + assert_eq!(invoice_nullable, db_invoice_nullable); +} + +#[cfg(test)] +async fn update_payments() { + use bitcoin::hashes::hex::{FromHex, ToHex}; + + const INVOICE: &str = "lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm"; + + let db = GlueDB::new( + #[cfg(target_arch = "wasm32")] + Some("update_payments".to_string()), + Arc::new(MutinyLogger::default()), + ) + .await + .unwrap(); + + let preimage: [u8; 32] = + FromHex::from_hex("7600f5a9ad72452dea7ad86dabbc9cb46be96a1a2fcd961e041d066b38d93008") + .unwrap(); + + let payment_hash = + sha256::Hash::from_hex("55ecf9169a6fa07e8ba181fdddf5b0bcc7860176659fa22a7cca9da2a359a33b") + .unwrap(); + + let pubkey = + PublicKey::from_str("02465ed5be53d04fde66c9418ff14a5f2267723810176c9212b722e542dc1afb1b") + .unwrap(); + + let i = Bolt11Invoice::from_str(INVOICE).unwrap(); + + let label = "test".to_string(); + let labels = vec![label.clone()]; + + let invoice1: MutinyInvoice = MutinyInvoice { + bolt11: Some(i), + description: Some("dest".to_string()), + payment_hash, + preimage: Some(preimage.to_hex()), + payee_pubkey: Some(pubkey), + amount_sats: Some(100), + expire: 1681781585, + status: HTLCStatus::Pending, + fees_paid: Some(1), + inbound: false, + labels: labels.clone(), + last_updated: 1681781585, + }; + db.save_payment(invoice1.clone()).await.unwrap(); + + let db_invoice = db.get_payment(&payment_hash).await.unwrap().unwrap(); + assert_eq!(invoice1, db_invoice); + + let db_invoices = db.list_payments().await.unwrap(); + assert_eq!(db_invoices.len(), 1); + assert_eq!(invoice1, db_invoices[0]); + + // Test update_payment_status + let new_status = HTLCStatus::Succeeded; + db.update_payment_status(&payment_hash, new_status.clone()) + .await + .unwrap(); + let updated_invoice = db.get_payment(&payment_hash).await.unwrap().unwrap(); + assert_eq!(updated_invoice.status, new_status); + + // Test update_payment_fee + let new_fee = Some(10u64); + db.update_payment_fee(&payment_hash, new_fee).await.unwrap(); + let updated_invoice_fee = db.get_payment(&payment_hash).await.unwrap().unwrap(); + assert_eq!(updated_invoice_fee.fees_paid, new_fee); + + // Test update_payment_preimage + let new_preimage: Option = + Some("0600f5a9ad72452dea7ad86dabbc9cb46be96a1a2fcd961e041d066b38d93008".to_string()); + db.update_payment_preimage(&payment_hash, new_preimage.clone()) + .await + .unwrap(); + let updated_invoice_preimage = db.get_payment(&payment_hash).await.unwrap().unwrap(); + assert_eq!(updated_invoice_preimage.preimage, new_preimage); + + // Test to make the timestamp was updated + assert!(updated_invoice.last_updated <= updated_invoice_fee.last_updated); + assert!(updated_invoice_fee.last_updated <= updated_invoice_preimage.last_updated); +} + #[cfg(test)] async fn create_glue_storage_value() { let db = GlueDB::new( @@ -561,6 +1153,18 @@ mod tests { async fn create_glue_storage_value_tests() { create_glue_storage_value().await; } + + #[cfg(feature = "ignored_tests")] + #[tokio::test] + async fn create_glue_and_payments_storage_tests() { + create_glue_and_payments_storage().await; + } + + #[cfg(feature = "ignored_tests")] + #[tokio::test] + async fn update_payments_tests() { + update_payments().await; + } } #[cfg(test)] @@ -591,4 +1195,14 @@ mod wasm_tests { async fn create_glue_storage_value_tests() { create_glue_storage_value().await; } + + #[test] + async fn create_glue_and_payments_storage_tests() { + create_glue_and_payments_storage().await; + } + + #[test] + async fn update_payments_tests() { + update_payments().await; + } } diff --git a/mutiny-core/src/sql/mod.rs b/mutiny-core/src/sql/mod.rs index a55d14889..1c67456aa 100644 --- a/mutiny-core/src/sql/mod.rs +++ b/mutiny-core/src/sql/mod.rs @@ -1 +1,27 @@ +use crate::{error::MutinyError, nodemanager::MutinyInvoice, HTLCStatus}; + pub mod glue; + +pub(crate) trait ApplicationStore { + async fn save_payment(&self, i: MutinyInvoice) -> Result<(), MutinyError>; + async fn get_payment( + &self, + payment_hash: &bitcoin::hashes::sha256::Hash, + ) -> Result, MutinyError>; + async fn update_payment_status( + &self, + payment_hash: &bitcoin::hashes::sha256::Hash, + status: HTLCStatus, + ) -> Result<(), MutinyError>; + async fn update_payment_fee( + &self, + payment_hash: &bitcoin::hashes::sha256::Hash, + fee: Option, + ) -> Result<(), MutinyError>; + async fn update_payment_preimage( + &self, + payment_hash: &bitcoin::hashes::sha256::Hash, + preimage: Option, + ) -> Result<(), MutinyError>; + async fn list_payments(&self) -> Result, MutinyError>; +} From b531075e84d74aa751104038e4b200eef78744bb Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Wed, 20 Dec 2023 12:16:15 -0600 Subject: [PATCH 10/12] Fedimint cleanup --- mutiny-core/src/federation.rs | 36 +++-------------------------------- mutiny-core/src/lib.rs | 17 ++++------------- mutiny-core/src/sql/glue.rs | 8 -------- mutiny-core/src/sql/mod.rs | 4 ++++ 4 files changed, 11 insertions(+), 54 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index 4fd9b4aa6..cf60174ed 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -5,7 +5,6 @@ use crate::{ nodemanager::MutinyInvoice, onchain::coin_type_from_network, sql::{glue::GlueDB, ApplicationStore}, - storage::MutinyStorage, utils::{self, sleep}, ActivityItem, HTLCStatus, DEFAULT_PAYMENT_TIMEOUT, }; @@ -46,11 +45,7 @@ use futures_util::{pin_mut, StreamExt}; use lightning::{log_debug, log_error, log_info, log_trace, log_warn, util::logger::Logger}; use lightning_invoice::Bolt11Invoice; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{ - collections::HashMap, - fmt::Debug, - sync::{atomic::AtomicBool, Arc, RwLock}, -}; +use std::{collections::HashMap, fmt::Debug, sync::Arc}; // The amount of time in milliseconds to wait for // checking the status of a fedimint payment. This @@ -133,44 +128,25 @@ pub struct FedimintBalance { pub amount: u64, } -// TODO remove -#[allow(dead_code)] -pub(crate) struct FederationClient { +pub(crate) struct FederationClient { pub(crate) uuid: String, - pub(crate) federation_index: FederationIndex, - pub(crate) federation_code: InviteCode, pub(crate) fedimint_client: ClientArc, - stopped_components: Arc>>, - storage: S, g: GlueDB, - network: Network, pub(crate) logger: Arc, - stop: Arc, } -// TODO remove -#[allow(dead_code)] -impl FederationClient { +impl FederationClient { #[allow(clippy::too_many_arguments)] pub(crate) async fn new( uuid: String, - federation_index: &FederationIndex, federation_code: InviteCode, xprivkey: ExtendedPrivKey, - storage: S, g: GlueDB, network: Network, logger: Arc, - stop: Arc, ) -> Result { log_info!(logger, "initializing a new federation client: {uuid}"); - // a list of components that need to be stopped and whether or not they are stopped - // TODO remove this if we end up not needing to stop things - let stopped_components = Arc::new(RwLock::new(vec![])); - - log_info!(logger, "Joining federation {}", federation_code); - let federation_info = FederationInfo::from_invite_code(federation_code.clone()).await?; let mut client_builder = fedimint_client::Client::builder(); @@ -201,15 +177,9 @@ impl FederationClient { log_debug!(logger, "Built fedimint client"); Ok(FederationClient { uuid, - federation_index: federation_index.clone(), - federation_code, fedimint_client, - stopped_components, - storage, g, - network, logger, - stop, }) } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 8f1658087..446d71d54 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -287,7 +287,7 @@ pub struct MutinyWallet { pub node_manager: Arc>, pub nostr: Arc>, pub federation_storage: Arc>, - pub(crate) federations: Arc>>>>, + pub(crate) federations: Arc>>>, pub stop: Arc, pub logger: Arc, } @@ -343,7 +343,7 @@ impl MutinyWallet { // create federation library let (federation_storage, federations) = - create_federations(&storage, &config, glue_db.clone(), &logger, stop.clone()).await?; + create_federations(&storage, &config, glue_db.clone(), &logger).await?; let federation_storage = Arc::new(Mutex::new(federation_storage)); let federations = federations; @@ -1031,7 +1031,6 @@ impl MutinyWallet { self.federation_storage.clone(), self.federations.clone(), federation_code, - self.stop.clone(), ) .await } @@ -1121,11 +1120,10 @@ async fn create_federations( c: &MutinyWalletConfig, g: GlueDB, logger: &Arc, - stop: Arc, ) -> Result< ( FederationStorage, - Arc>>>>, + Arc>>>, ), MutinyError, > { @@ -1135,14 +1133,11 @@ async fn create_federations( for federation_item in federations { let federation = FederationClient::new( federation_item.0, - &federation_item.1, federation_item.1.federation_code.clone(), c.xprivkey, - storage.clone(), g.clone(), c.network, logger.clone(), - stop.clone(), ) .await?; @@ -1163,9 +1158,8 @@ pub(crate) async fn create_new_federation( network: Network, logger: Arc, federation_storage: Arc>, - federations: Arc>>>>, + federations: Arc>>>, federation_code: InviteCode, - stop: Arc, ) -> Result { // Begin with a mutex lock so that nothing else can // save or alter the federation list while it is about to @@ -1201,14 +1195,11 @@ pub(crate) async fn create_new_federation( // now create the federation process and init it let new_federation = FederationClient::new( next_federation_uuid.clone(), - &next_federation, federation_code, xprivkey, - storage.clone(), g.clone(), network, logger.clone(), - stop, ) .await?; diff --git a/mutiny-core/src/sql/glue.rs b/mutiny-core/src/sql/glue.rs index f42ed9167..2b48cf3e9 100644 --- a/mutiny-core/src/sql/glue.rs +++ b/mutiny-core/src/sql/glue.rs @@ -125,7 +125,6 @@ impl GlueDB { } impl ApplicationStore for GlueDB { - #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] async fn save_payment(&self, invoice: MutinyInvoice) -> Result<(), MutinyError> { #[cfg(not(target_arch = "wasm32"))] unimplemented!("can't run on servers until Send is supported in Glue"); @@ -201,7 +200,6 @@ impl ApplicationStore for GlueDB { } } - #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] async fn get_payment( &self, payment_hash: &bitcoin::hashes::sha256::Hash, @@ -255,7 +253,6 @@ impl ApplicationStore for GlueDB { } } - #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] async fn update_payment_status( &self, payment_hash: &bitcoin::hashes::sha256::Hash, @@ -284,7 +281,6 @@ impl ApplicationStore for GlueDB { } } - #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] async fn update_payment_fee( &self, payment_hash: &bitcoin::hashes::sha256::Hash, @@ -316,7 +312,6 @@ impl ApplicationStore for GlueDB { } } - #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code, unused_variables))] async fn update_payment_preimage( &self, payment_hash: &bitcoin::hashes::sha256::Hash, @@ -348,7 +343,6 @@ impl ApplicationStore for GlueDB { } } - #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] async fn list_payments(&self) -> Result, MutinyError> { #[cfg(not(target_arch = "wasm32"))] unimplemented!("can't run on servers until Send is supported in Glue"); @@ -382,7 +376,6 @@ impl ApplicationStore for GlueDB { } } -#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] fn parse_row_to_invoice( row: Vec, logger: Arc, @@ -598,7 +591,6 @@ impl IRawDatabase for FedimintDB { } } -#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] pub struct GluePseudoTransaction<'a> { #[cfg(not(target_arch = "wasm32"))] pub(crate) db: Arc>>, diff --git a/mutiny-core/src/sql/mod.rs b/mutiny-core/src/sql/mod.rs index 1c67456aa..cc92094f8 100644 --- a/mutiny-core/src/sql/mod.rs +++ b/mutiny-core/src/sql/mod.rs @@ -1,3 +1,7 @@ +#![cfg_attr( + not(target_arch = "wasm32"), + allow(unused_variables, dead_code, unused_imports) +)] use crate::{error::MutinyError, nodemanager::MutinyInvoice, HTLCStatus}; pub mod glue; From 60ecdedaccc507eac2840bede4d8e6f7a0036832 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Wed, 20 Dec 2023 12:49:05 -0600 Subject: [PATCH 11/12] Rename LnNode to InvoiceHandler and move to lib --- mutiny-core/src/lib.rs | 17 +++++++++++++++++ mutiny-core/src/node.rs | 23 ++++++----------------- mutiny-core/src/nostr/nwc.rs | 10 +++++----- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 446d71d54..7484090e4 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -87,8 +87,25 @@ use std::sync::Arc; use std::{collections::HashMap, sync::atomic::AtomicBool}; use uuid::Uuid; +#[cfg(test)] +use mockall::{automock, predicate::*}; + const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; +#[cfg_attr(test, automock)] +pub(crate) trait InvoiceHandler { + fn logger(&self) -> &MutinyLogger; + fn skip_hodl_invoices(&self) -> bool; + fn get_outbound_payment_status(&self, payment_hash: &[u8; 32]) -> Option; + async fn pay_invoice_with_timeout( + &self, + invoice: &Bolt11Invoice, + amt_sats: Option, + timeout_secs: Option, + labels: Vec, + ) -> Result; +} + #[derive(Copy, Clone)] pub struct MutinyBalance { pub confirmed: u64, diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index 71cf9ed88..25495bd86 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -1,4 +1,3 @@ -use crate::ldkstorage::{persist_monitor, ChannelOpenParams}; use crate::lsp::{InvoiceRequest, LspConfig}; use crate::messagehandler::MutinyMessageHandler; use crate::nodemanager::ChannelClosure; @@ -23,6 +22,10 @@ use crate::{ use crate::{fees::P2WSH_OUTPUT_SIZE, peermanager::connect_peer_if_necessary}; use crate::{keymanager::PhantomKeysManager, scorer::HubPreferentialScorer}; use crate::{labels::LabelStorage, DEFAULT_PAYMENT_TIMEOUT}; +use crate::{ + ldkstorage::{persist_monitor, ChannelOpenParams}, + InvoiceHandler, +}; use anyhow::{anyhow, Context}; use bdk::FeeRate; use bitcoin::hashes::{hex::ToHex, sha256::Hash as Sha256}; @@ -73,7 +76,7 @@ use lightning_liquidity::{ }; #[cfg(test)] -use mockall::{automock, predicate::*}; +use mockall::predicate::*; use std::collections::HashMap; use std::{ str::FromStr, @@ -2054,21 +2057,7 @@ pub(crate) fn default_user_config() -> UserConfig { } } -#[cfg_attr(test, automock)] -pub(crate) trait LnNode { - fn logger(&self) -> &MutinyLogger; - fn skip_hodl_invoices(&self) -> bool; - fn get_outbound_payment_status(&self, payment_hash: &[u8; 32]) -> Option; - async fn pay_invoice_with_timeout( - &self, - invoice: &Bolt11Invoice, - amt_sats: Option, - timeout_secs: Option, - labels: Vec, - ) -> Result; -} - -impl LnNode for Node { +impl InvoiceHandler for Node { fn logger(&self) -> &MutinyLogger { self.logger.as_ref() } diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index 17deb34d8..28221b0e1 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -1,10 +1,10 @@ use crate::error::MutinyError; use crate::event::HTLCStatus; -use crate::node::LnNode; use crate::nostr::nip49::NIP49Confirmation; use crate::nostr::NostrManager; use crate::storage::MutinyStorage; use crate::utils; +use crate::InvoiceHandler; use anyhow::anyhow; use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::secp256k1::{Secp256k1, Signing, ThirtyTwoByteHash}; @@ -309,7 +309,7 @@ impl NostrWalletConnect { pub(crate) async fn pay_nwc_invoice( &self, - node: &impl LnNode, + node: &impl InvoiceHandler, invoice: &Bolt11Invoice, ) -> Result { let label = self @@ -398,7 +398,7 @@ impl NostrWalletConnect { pub async fn handle_nwc_request( &mut self, event: Event, - node: &impl LnNode, + node: &impl InvoiceHandler, nostr_manager: &NostrManager, ) -> anyhow::Result> { let client_pubkey = self.client_key.public_key(); @@ -1102,11 +1102,11 @@ mod wasm_test { use super::*; use crate::event::{MillisatAmount, PaymentInfo}; use crate::logging::MutinyLogger; - use crate::node::MockLnNode; use crate::nodemanager::MutinyInvoice; use crate::nostr::ProfileType; use crate::storage::MemoryStorage; use crate::test_utils::{create_dummy_invoice, create_node, create_nwc_request}; + use crate::MockInvoiceHandler; use bitcoin::hashes::Hash; use bitcoin::secp256k1::ONE_KEY; use bitcoin::Network; @@ -1454,7 +1454,7 @@ mod wasm_test { async fn test_process_nwc_event_budget() { let storage = MemoryStorage::default(); let logger = Arc::new(MutinyLogger::default()); - let mut node = MockLnNode::new(); + let mut node = MockInvoiceHandler::new(); let amount_msats = 5_000; From 2a53516da230f0c7c7c5f0d78491d5f99b8d6c50 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Wed, 20 Dec 2023 15:56:45 -0600 Subject: [PATCH 12/12] Refactor NWC to take MutinyWallet --- mutiny-core/src/lib.rs | 153 +++++++++++++++++++++------------ mutiny-core/src/node.rs | 37 +------- mutiny-core/src/nodemanager.rs | 4 - mutiny-core/src/nostr/mod.rs | 108 ++++++++++++++--------- mutiny-core/src/nostr/nwc.rs | 118 +++++++++++++------------ mutiny-core/src/test_utils.rs | 30 ++++++- mutiny-wasm/src/lib.rs | 12 +-- 7 files changed, 262 insertions(+), 200 deletions(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 7484090e4..f1cfd304d 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -71,9 +71,8 @@ use ::nostr::{Event, Kind, Metadata}; use bdk_chain::ConfirmationTime; use bip39::Mnemonic; use bitcoin::util::bip32::ExtendedPrivKey; -use bitcoin::Network; -use bitcoin::{hashes::sha256, secp256k1::PublicKey}; -use fedimint_core::{api::InviteCode, config::FederationId}; +use bitcoin::{hashes::sha256, Network}; +use fedimint_core::{api::InviteCode, config::FederationId, BitcoinHash}; use futures::{pin_mut, select, FutureExt}; use futures_util::lock::Mutex; use lightning::{log_debug, util::logger::Logger}; @@ -93,15 +92,19 @@ use mockall::{automock, predicate::*}; const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; #[cfg_attr(test, automock)] -pub(crate) trait InvoiceHandler { +pub trait InvoiceHandler { fn logger(&self) -> &MutinyLogger; fn skip_hodl_invoices(&self) -> bool; - fn get_outbound_payment_status(&self, payment_hash: &[u8; 32]) -> Option; - async fn pay_invoice_with_timeout( + async fn get_outbound_payment_status(&self, payment_hash: &[u8; 32]) -> Option; + async fn pay_invoice( &self, invoice: &Bolt11Invoice, amt_sats: Option, - timeout_secs: Option, + labels: Vec, + ) -> Result; + async fn create_invoice( + &self, + amount: Option, labels: Vec, ) -> Result; } @@ -348,6 +351,7 @@ impl MutinyWallet { node_manager.xprivkey, storage.clone(), node_manager.logger.clone(), + stop.clone(), )?); // create gluedb storage @@ -399,15 +403,15 @@ impl MutinyWallet { } // if we don't have any nodes, create one - let first_node = { - match mw.node_manager.list_nodes().await?.pop() { - Some(node) => node, - None => mw.node_manager.new_node().await?.pubkey, + match mw.node_manager.list_nodes().await?.pop() { + Some(_) => (), + None => { + mw.node_manager.new_node().await?; } }; // start the nostr wallet connect background process - mw.start_nostr_wallet_connect(first_node).await; + mw.start_nostr_wallet_connect().await; Ok(mw) } @@ -437,12 +441,14 @@ impl MutinyWallet { } /// Starts a background process that will watch for nostr wallet connect events - pub(crate) async fn start_nostr_wallet_connect(&self, from_node: PublicKey) { + pub(crate) async fn start_nostr_wallet_connect(&self) { let nostr = self.nostr.clone(); - let nm = self.node_manager.clone(); + let logger = self.logger.clone(); + let stop = self.stop.clone(); + let self_clone = self.clone(); utils::spawn(async move { loop { - if nm.stop.load(Ordering::Relaxed) { + if stop.load(Ordering::Relaxed) { break; }; @@ -457,19 +463,20 @@ impl MutinyWallet { // clear in-active profiles, we used to have disabled and archived profiles // but now we just delete profiles if let Err(e) = nostr.remove_inactive_profiles() { - log_warn!(nm.logger, "Failed to clear in-active NWC profiles: {e}"); + log_warn!(logger, "Failed to clear in-active NWC profiles: {e}"); } // if a single-use profile's payment was successful in the background, // we can safely clear it now - let node = nm.get_node(&from_node).await.expect("failed to get node"); - if let Err(e) = nostr.clear_successful_single_use_profiles(&node) { - log_warn!(nm.logger, "Failed to clear in-active NWC profiles: {e}"); + if let Err(e) = nostr + .clear_successful_single_use_profiles(&self_clone) + .await + { + log_warn!(logger, "Failed to clear in-active NWC profiles: {e}"); } - drop(node); if let Err(e) = nostr.clear_expired_nwc_invoices().await { - log_warn!(nm.logger, "Failed to clear expired NWC invoices: {e}"); + log_warn!(logger, "Failed to clear expired NWC invoices: {e}"); } // clear successful single-use profiles @@ -514,15 +521,15 @@ impl MutinyWallet { match notification { Ok(RelayPoolNotification::Event(_url, event)) => { if event.kind == Kind::WalletConnectRequest && event.verify().is_ok() { - match nostr.handle_nwc_request(event, &nm, &from_node).await { + match nostr.handle_nwc_request(event, &self_clone).await { Ok(Some(event)) => { if let Err(e) = client.send_event(event).await { - log_warn!(nm.logger, "Error sending NWC event: {e}"); + log_warn!(logger, "Error sending NWC event: {e}"); } } Ok(None) => {} // no response Err(e) => { - log_error!(nm.logger, "Error handling NWC request: {e}"); + log_error!(logger, "Error handling NWC request: {e}"); } } } @@ -535,7 +542,7 @@ impl MutinyWallet { } } _ = delay_fut => { - if nm.stop.load(Ordering::Relaxed) { + if stop.load(Ordering::Relaxed) { break; } } @@ -543,7 +550,7 @@ impl MutinyWallet { // Check if the filters have changed let current_filters = nostr.get_nwc_filters(); if current_filters != last_filters { - log_debug!(nm.logger, "subscribing to new nwc filters"); + log_debug!(logger, "subscribing to new nwc filters"); client.subscribe(current_filters.clone()).await; last_filters = current_filters; } @@ -554,7 +561,7 @@ impl MutinyWallet { } if let Err(e) = client.disconnect().await { - log_warn!(nm.logger, "Error disconnecting from relays: {e}"); + log_warn!(logger, "Error disconnecting from relays: {e}"); } } }); @@ -653,34 +660,10 @@ impl MutinyWallet { let invoice = if self.config.safe_mode { None } else { - // Check if a federation exists - let federation_ids = self.list_federation_ids().await?; - if !federation_ids.is_empty() { - // Use the first federation for simplicity - let federation_id = &federation_ids[0]; - let fedimint_client = self.federations.lock().await.get(federation_id).cloned(); - - match fedimint_client { - Some(client) => { - // Try to create an invoice using the federation - match client - .get_invoice(amount.unwrap_or_default(), labels.clone()) - .await - { - Ok(inv) => Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?), - Err(_) => None, // Handle the error or fallback to node_manager invoice creation - } - } - None => None, // No federation client found, fallback to node_manager invoice creation - } - } else { - // Fallback to node_manager invoice creation if no federation is found - let inv = self - .node_manager - .create_invoice(amount, labels.clone()) - .await?; - Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?) - } + self.create_lightning_invoice(amount, labels.clone()) + .await + .ok() + .and_then(|invoice| invoice.bolt11) }; let Ok(address) = self.node_manager.get_new_address(labels.clone()) else { @@ -695,6 +678,32 @@ impl MutinyWallet { }) } + async fn create_lightning_invoice( + &self, + amount: Option, + labels: Vec, + ) -> Result { + let federation_ids = self.list_federation_ids().await?; + + // Attempt to create federation invoice + if !federation_ids.is_empty() { + let federation_id = &federation_ids[0]; + let fedimint_client = self.federations.lock().await.get(federation_id).cloned(); + + if let Some(client) = fedimint_client { + if let Ok(inv) = client + .get_invoice(amount.unwrap_or_default(), labels.clone()) + .await + { + return Ok(inv); + } + } + } + + // Fallback to node_manager invoice creation if no federation invoice created + self.node_manager.create_invoice(amount, labels).await + } + /// Gets the current balance of the wallet. /// This includes both on-chain, lightning funds, and federations. /// @@ -1132,6 +1141,40 @@ impl MutinyWallet { } } +impl InvoiceHandler for MutinyWallet { + fn logger(&self) -> &MutinyLogger { + self.logger.as_ref() + } + + fn skip_hodl_invoices(&self) -> bool { + self.config.skip_hodl_invoices + } + + async fn get_outbound_payment_status(&self, payment_hash: &[u8; 32]) -> Option { + self.get_invoice_by_hash(&sha256::Hash::hash(payment_hash)) + .await + .ok() + .map(|p| p.status) + } + + async fn pay_invoice( + &self, + invoice: &Bolt11Invoice, + amt_sats: Option, + labels: Vec, + ) -> Result { + self.pay_invoice(invoice, amt_sats, labels).await + } + + async fn create_invoice( + &self, + amount: Option, + labels: Vec, + ) -> Result { + self.create_lightning_invoice(amount, labels).await + } +} + async fn create_federations( storage: &S, c: &MutinyWalletConfig, diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index 25495bd86..5ecbe5207 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -1,3 +1,4 @@ +use crate::ldkstorage::{persist_monitor, ChannelOpenParams}; use crate::lsp::{InvoiceRequest, LspConfig}; use crate::messagehandler::MutinyMessageHandler; use crate::nodemanager::ChannelClosure; @@ -22,10 +23,6 @@ use crate::{ use crate::{fees::P2WSH_OUTPUT_SIZE, peermanager::connect_peer_if_necessary}; use crate::{keymanager::PhantomKeysManager, scorer::HubPreferentialScorer}; use crate::{labels::LabelStorage, DEFAULT_PAYMENT_TIMEOUT}; -use crate::{ - ldkstorage::{persist_monitor, ChannelOpenParams}, - InvoiceHandler, -}; use anyhow::{anyhow, Context}; use bdk::FeeRate; use bitcoin::hashes::{hex::ToHex, sha256::Hash as Sha256}; @@ -186,7 +183,6 @@ pub(crate) struct Node { pub(crate) lsp_client: Option>, pub(crate) sync_lock: Arc>, stop: Arc, - pub skip_hodl_invoices: bool, #[cfg(target_arch = "wasm32")] websocket_proxy_addr: String, } @@ -209,7 +205,6 @@ impl Node { logger: Arc, do_not_connect_peers: bool, empty_state: bool, - skip_hodl_invoices: bool, #[cfg(target_arch = "wasm32")] websocket_proxy_addr: String, ) -> Result { log_info!(logger, "initializing a new node: {uuid}"); @@ -728,7 +723,6 @@ impl Node { lsp_client, sync_lock, stop, - skip_hodl_invoices, #[cfg(target_arch = "wasm32")] websocket_proxy_addr, }) @@ -1160,7 +1154,7 @@ impl Node { .read_payment_info(payment_hash.as_inner(), false, &self.logger) { Some(payment_info) => Ok((payment_info, false)), - None => Err(MutinyError::InvoiceInvalid), + None => Err(MutinyError::NotFound), } } @@ -2057,33 +2051,6 @@ pub(crate) fn default_user_config() -> UserConfig { } } -impl InvoiceHandler for Node { - fn logger(&self) -> &MutinyLogger { - self.logger.as_ref() - } - - fn skip_hodl_invoices(&self) -> bool { - self.skip_hodl_invoices - } - - fn get_outbound_payment_status(&self, payment_hash: &[u8; 32]) -> Option { - self.persister - .read_payment_info(payment_hash, false, &self.logger) - .map(|p| p.status) - } - - async fn pay_invoice_with_timeout( - &self, - invoice: &Bolt11Invoice, - amt_sats: Option, - timeout_secs: Option, - labels: Vec, - ) -> Result { - self.pay_invoice_with_timeout(invoice, amt_sats, timeout_secs, labels) - .await - } -} - #[cfg(test)] #[cfg(not(target_arch = "wasm32"))] mod tests { diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 0155979d8..be9458779 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -433,7 +433,6 @@ pub struct NodeManager { pub(crate) logger: Arc, bitcoin_price_cache: Arc>>, do_not_connect_peers: bool, - skip_hodl_invoices: bool, pub safe_mode: bool, } @@ -580,7 +579,6 @@ impl NodeManager { logger.clone(), c.do_not_connect_peers, false, - c.skip_hodl_invoices, #[cfg(target_arch = "wasm32")] websocket_proxy_addr.clone(), ) @@ -667,7 +665,6 @@ impl NodeManager { bitcoin_price_cache: Arc::new(Mutex::new(price_cache)), do_not_connect_peers: c.do_not_connect_peers, safe_mode: c.safe_mode, - skip_hodl_invoices: c.skip_hodl_invoices, }; Ok(nm) @@ -2382,7 +2379,6 @@ pub(crate) async fn create_new_node_from_node_manager( node_manager.logger.clone(), node_manager.do_not_connect_peers, false, - node_manager.skip_hodl_invoices, #[cfg(target_arch = "wasm32")] node_manager.websocket_proxy_addr.clone(), ) diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 79860a283..df1a55931 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -1,7 +1,4 @@ -use crate::labels::LabelStorage; use crate::logging::MutinyLogger; -use crate::node::Node; -use crate::nodemanager::NodeManager; use crate::nostr::nip49::{NIP49BudgetPeriod, NIP49URI}; use crate::nostr::nwc::{ BudgetPeriod, BudgetedSpendingConditions, NostrWalletConnect, NwcProfile, NwcProfileTag, @@ -10,11 +7,15 @@ use crate::nostr::nwc::{ }; use crate::storage::MutinyStorage; use crate::{error::MutinyError, utils::get_random_bip32_child_index}; +use crate::{labels::LabelStorage, InvoiceHandler}; use crate::{utils, HTLCStatus}; -use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::{sha256, Hash}; -use bitcoin::secp256k1::{PublicKey, Secp256k1, Signing}; +use bitcoin::secp256k1::{Secp256k1, Signing}; use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}; +use bitcoin::{ + hashes::hex::{FromHex, ToHex}, + secp256k1::ThirtyTwoByteHash, +}; use futures::{pin_mut, select, FutureExt}; use futures_util::lock::Mutex; use lightning::util::logger::Logger; @@ -24,10 +25,9 @@ use nostr::nips::nip47::*; use nostr::prelude::{decrypt, encrypt}; use nostr::{Event, EventBuilder, EventId, Filter, Keys, Kind, Tag}; use nostr_sdk::{Client, RelayPoolNotification}; -use std::str::FromStr; -use std::sync::atomic::Ordering; -use std::sync::{Arc, RwLock}; +use std::sync::{atomic::Ordering, Arc, RwLock}; use std::time::Duration; +use std::{str::FromStr, sync::atomic::AtomicBool}; pub mod nip49; pub mod nwc; @@ -76,6 +76,8 @@ pub struct NostrManager { pending_nwc_lock: Arc>, /// Logger pub logger: Arc, + /// Atomic stop signal + pub stop: Arc, } impl NostrManager { @@ -151,30 +153,58 @@ impl NostrManager { } /// Goes through all single use profiles and removes the successfully paid ones - pub(crate) fn clear_successful_single_use_profiles( + pub(crate) async fn clear_successful_single_use_profiles( &self, - node: &Node, + invoice_handler: &impl InvoiceHandler, ) -> Result<(), MutinyError> { - let mut profiles = self.nwc.write().unwrap(); - - profiles.retain(|x| { - if let SpendingConditions::SingleUse(single_use) = &x.profile.spending_conditions { - if let Some(payment_hash) = &single_use.payment_hash { - let hash: [u8; 32] = FromHex::from_hex(payment_hash).expect("invalid hash"); - if let Some(payment) = - node.persister.read_payment_info(&hash, false, &self.logger) + // Go through all remaining Single Use NWC + let indices_to_remove = { + let profiles = self.nwc.write().unwrap(); + profiles + .iter() + .enumerate() + .filter_map(|(index, x)| { + if let SpendingConditions::SingleUse(single_use) = + &x.profile.spending_conditions { - if payment.status == HTLCStatus::Succeeded { - return false; + if let Some(payment_hash) = &single_use.payment_hash { + match FromHex::from_hex(payment_hash) { + Ok(hash) => { + let hash: [u8; 32] = hash; + Some((index, hash)) + } + Err(_) => None, + } + } else { + None } + } else { + None } + }) + .collect::>() + }; + + // All futures to go check on the status of those single use NWC + let futures: Vec<_> = indices_to_remove + .into_iter() + .map(|(index, hash)| async move { + match invoice_handler.get_outbound_payment_status(&hash).await { + Some(HTLCStatus::Succeeded) => Some(index), + _ => None, } - } - true - }); + }) + .collect(); - // save to storage + let results = futures::future::join_all(futures).await; + + // Remove all of those NWC and then save { + let mut profiles = self.nwc.write().unwrap(); + for index in results.into_iter().flatten().rev() { + profiles.remove(index); + } + let profiles = profiles .iter() .map(|x| x.profile.clone()) @@ -586,13 +616,11 @@ impl NostrManager { pub async fn approve_invoice( &self, hash: sha256::Hash, - node_manager: &NodeManager, - from_node: &PublicKey, + invoice_handler: &impl InvoiceHandler, ) -> Result { let (nwc, inv) = self.find_nwc_data(&hash)?; - let node = node_manager.get_node(from_node).await?; - let resp = nwc.pay_nwc_invoice(node.as_ref(), &inv.invoice).await?; + let resp = nwc.pay_nwc_invoice(invoice_handler, &inv.invoice).await?; let event_id = self.broadcast_nwc_response(resp, nwc, inv).await?; @@ -746,8 +774,7 @@ impl NostrManager { pub async fn handle_nwc_request( &self, event: Event, - node_manager: &NodeManager, - from_node: &PublicKey, + invoice_handler: &impl InvoiceHandler, ) -> anyhow::Result> { let nwc = { let vec = self.nwc.read().unwrap(); @@ -757,8 +784,7 @@ impl NostrManager { }; if let Some(mut nwc) = nwc { - let node = node_manager.get_node(from_node).await?; - let event = nwc.handle_nwc_request(event, node.as_ref(), self).await?; + let event = nwc.handle_nwc_request(event, invoice_handler, self).await?; Ok(event) } else { Ok(None) @@ -802,7 +828,7 @@ impl NostrManager { &self, amount_sats: u64, nwc_uri: &str, - node_manager: &NodeManager, + invoice_handler: &impl InvoiceHandler, ) -> Result, MutinyError> { let nwc = NostrWalletConnectURI::from_str(nwc_uri) .map_err(|_| MutinyError::InvalidArgumentsError)?; @@ -818,7 +844,7 @@ impl NostrManager { add_relay_res.expect("Failed to add relays"); client.connect().await; - let invoice = node_manager + let invoice = invoice_handler .create_invoice(Some(amount_sats), vec!["Gift".to_string()]) .await?; // unwrap is safe, we just created it @@ -864,11 +890,11 @@ impl NostrManager { // check if the invoice has been paid, if so, return, otherwise continue // checking for response event - if let Ok(invoice) = node_manager - .get_invoice_by_hash(bolt11.payment_hash()) + if let Some(status) = invoice_handler + .get_outbound_payment_status(&bolt11.payment_hash().into_32()) .await { - if invoice.paid() { + if status == HTLCStatus::Succeeded { break; } } @@ -918,7 +944,7 @@ impl NostrManager { } } _ = delay_fut => { - if node_manager.stop.load(Ordering::Relaxed) { + if self.stop.load(Ordering::Relaxed) { client.disconnect().await?; return Err(MutinyError::NotRunning); } @@ -986,6 +1012,7 @@ impl NostrManager { xprivkey: ExtendedPrivKey, storage: S, logger: Arc, + stop: Arc, ) -> Result { let context = Secp256k1::new(); @@ -1008,6 +1035,7 @@ impl NostrManager { storage, pending_nwc_lock: Arc::new(Mutex::new(())), logger, + stop, }) } } @@ -1061,7 +1089,9 @@ mod test { let logger = Arc::new(MutinyLogger::default()); - NostrManager::from_mnemonic(xprivkey, storage, logger).unwrap() + let stop = Arc::new(AtomicBool::new(false)); + + NostrManager::from_mnemonic(xprivkey, storage, logger, stop).unwrap() } #[test] diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index 28221b0e1..96b16f06a 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -317,10 +317,7 @@ impl NostrWalletConnect { .label .clone() .unwrap_or(self.profile.name.clone()); - match node - .pay_invoice_with_timeout(invoice, None, None, vec![label]) - .await - { + match node.pay_invoice(invoice, None, vec![label]).await { Ok(inv) => { // preimage should be set after a successful payment let preimage = inv.preimage.expect("preimage not set"); @@ -463,6 +460,7 @@ impl NostrWalletConnect { // if we have already paid or are attempting to pay this invoice, skip it if node .get_outbound_payment_status(&invoice.payment_hash().into_32()) + .await .is_some_and(|status| { matches!(status, HTLCStatus::Succeeded | HTLCStatus::InFlight) }) @@ -480,7 +478,7 @@ impl NostrWalletConnect { Some(payment_hash) => { let hash: [u8; 32] = FromHex::from_hex(&payment_hash).expect("invalid hash"); - node.get_outbound_payment_status(&hash) + node.get_outbound_payment_status(&hash).await } None => None, }; @@ -612,13 +610,28 @@ impl NostrWalletConnect { } else if budget.sum_payments() + sats > budget.budget { // budget might not actually be exceeded, we should verify that the payments // all went through, and if not, remove them from the budget - budget.payments.retain(|p| { - let hash: [u8; 32] = FromHex::from_hex(&p.hash).unwrap(); - match node.get_outbound_payment_status(&hash) { - Some(status) => status != HTLCStatus::Failed, // remove failed payments from budget - None => true, // if we can't find the payment, keep it to be safe - } - }); + let mut indices_to_remove = Vec::new(); + for (index, p) in budget.payments.iter().enumerate() { + let hash: [u8; 32] = FromHex::from_hex(&p.hash)?; + indices_to_remove.push((index, hash)); + } + + let futures: Vec<_> = indices_to_remove + .iter() + .map(|(index, hash)| async move { + match node.get_outbound_payment_status(hash).await { + Some(HTLCStatus::Failed) => Some(*index), + _ => None, + } + }) + .collect(); + + let results = futures::future::join_all(futures).await; + + // Remove failed payments + for index in results.into_iter().flatten().rev() { + budget.payments.remove(index); + } // update budget with removed payments self.profile.spending_conditions = @@ -1100,18 +1113,17 @@ mod test { #[cfg(target_arch = "wasm32")] mod wasm_test { use super::*; - use crate::event::{MillisatAmount, PaymentInfo}; use crate::logging::MutinyLogger; use crate::nodemanager::MutinyInvoice; use crate::nostr::ProfileType; use crate::storage::MemoryStorage; - use crate::test_utils::{create_dummy_invoice, create_node, create_nwc_request}; + use crate::test_utils::{create_dummy_invoice, create_mutiny_wallet, create_nwc_request}; use crate::MockInvoiceHandler; - use bitcoin::hashes::Hash; use bitcoin::secp256k1::ONE_KEY; use bitcoin::Network; + use mockall::predicate::eq; use nostr::key::SecretKey; - use std::sync::Arc; + use std::sync::{atomic::AtomicBool, Arc}; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; wasm_bindgen_test_configure!(run_in_browser); @@ -1142,12 +1154,14 @@ mod wasm_test { #[test] async fn test_allowed_hodl_invoice() { let storage = MemoryStorage::default(); - let mut node = create_node(storage.clone()).await; - node.skip_hodl_invoices = false; // allow hodl invoices + let mut mw = create_mutiny_wallet(storage.clone()).await; + mw.config.skip_hodl_invoices = false; // allow hodl invoices let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); + let stop = Arc::new(AtomicBool::new(false)); let nostr_manager = - NostrManager::from_mnemonic(xprivkey, storage.clone(), node.logger.clone()).unwrap(); + NostrManager::from_mnemonic(xprivkey, storage.clone(), mw.logger.clone(), stop) + .unwrap(); let profile = nostr_manager .create_new_profile( @@ -1169,7 +1183,7 @@ mod wasm_test { .to_string(); let event = create_nwc_request(&uri, invoice.clone()); let result = nwc - .handle_nwc_request(event.clone(), &node, &nostr_manager) + .handle_nwc_request(event.clone(), &mw, &nostr_manager) .await; assert_eq!(result.unwrap(), None); @@ -1187,11 +1201,15 @@ mod wasm_test { #[test] async fn test_process_nwc_event_require_approval() { let storage = MemoryStorage::default(); - let node = create_node(storage.clone()).await; + let logger = Arc::new(MutinyLogger::default()); + let mut node = MockInvoiceHandler::new(); + node.expect_logger().return_const(MutinyLogger::default()); + storage.set_done_first_sync().unwrap(); let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); + let stop = Arc::new(AtomicBool::new(false)); let nostr_manager = - NostrManager::from_mnemonic(xprivkey, storage.clone(), node.logger.clone()).unwrap(); + NostrManager::from_mnemonic(xprivkey, storage.clone(), logger.clone(), stop).unwrap(); let profile = nostr_manager .create_new_profile( @@ -1268,6 +1286,7 @@ mod wasm_test { check_no_pending_invoices(&storage); // test hodl invoice + node.expect_skip_hodl_invoices().return_const(true); let invoice = create_dummy_invoice(Some(10_000), Network::Regtest, Some(ONE_KEY)) .0 .to_string(); @@ -1285,19 +1304,9 @@ mod wasm_test { // test in-flight payment let (invoice, _) = create_dummy_invoice(Some(1_000), Network::Regtest, None); - let payment_info = PaymentInfo { - preimage: None, - secret: Some(invoice.payment_secret().0), - status: HTLCStatus::InFlight, - amt_msat: MillisatAmount(invoice.amount_milli_satoshis()), - fee_paid_msat: None, - bolt11: Some(invoice.clone()), - payee_pubkey: None, - last_update: utils::now().as_secs(), - }; - node.persister - .persist_payment_info(invoice.payment_hash().as_inner(), &payment_info, false) - .unwrap(); + node.expect_get_outbound_payment_status() + .with(eq(invoice.payment_hash().into_32())) + .returning(move |_| Some(HTLCStatus::InFlight)); let event = create_nwc_request(&uri, invoice.to_string()); let result = nwc.handle_nwc_request(event, &node, &nostr_manager).await; assert_eq!(result.unwrap(), None); @@ -1305,19 +1314,9 @@ mod wasm_test { // test completed payment let (invoice, _) = create_dummy_invoice(Some(1_000), Network::Regtest, None); - let payment_info = PaymentInfo { - preimage: None, - secret: Some(invoice.payment_secret().0), - status: HTLCStatus::Succeeded, - amt_msat: MillisatAmount(invoice.amount_milli_satoshis()), - fee_paid_msat: None, - bolt11: Some(invoice.clone()), - payee_pubkey: None, - last_update: utils::now().as_secs(), - }; - node.persister - .persist_payment_info(invoice.payment_hash().as_inner(), &payment_info, false) - .unwrap(); + node.expect_get_outbound_payment_status() + .with(eq(invoice.payment_hash().into_32())) + .returning(move |_| Some(HTLCStatus::Succeeded)); let event = create_nwc_request(&uri, invoice.to_string()); let result = nwc.handle_nwc_request(event, &node, &nostr_manager).await; assert_eq!(result.unwrap(), None); @@ -1325,6 +1324,9 @@ mod wasm_test { // test it goes to pending let (invoice, _) = create_dummy_invoice(Some(1_000), Network::Regtest, None); + node.expect_get_outbound_payment_status() + .with(eq(invoice.payment_hash().into_32())) + .returning(move |_| None); let event = create_nwc_request(&uri, invoice.to_string()); let result = nwc .handle_nwc_request(event.clone(), &node, &nostr_manager) @@ -1346,10 +1348,12 @@ mod wasm_test { async fn test_clear_expired_pending_invoices() { let storage = MemoryStorage::default(); let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); + let stop = Arc::new(AtomicBool::new(false)); let nostr_manager = NostrManager::from_mnemonic( xprivkey, storage.clone(), Arc::new(MutinyLogger::default()), + stop, ) .unwrap(); @@ -1392,11 +1396,13 @@ mod wasm_test { #[test] async fn test_failed_process_nwc_event_budget() { let storage = MemoryStorage::default(); - let node = create_node(storage.clone()).await; + let mw = create_mutiny_wallet(storage.clone()).await; let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); + let stop = Arc::new(AtomicBool::new(false)); let nostr_manager = - NostrManager::from_mnemonic(xprivkey, storage.clone(), node.logger.clone()).unwrap(); + NostrManager::from_mnemonic(xprivkey, storage.clone(), mw.logger.clone(), stop) + .unwrap(); let budget = 10_000; let profile = nostr_manager @@ -1422,7 +1428,7 @@ mod wasm_test { let (invoice, _) = create_dummy_invoice(Some(10), Network::Regtest, None); let event = create_nwc_request(&uri, invoice.to_string()); let result = nwc - .handle_nwc_request(event.clone(), &node, &nostr_manager) + .handle_nwc_request(event.clone(), &mw, &nostr_manager) .await; assert!(result.unwrap().is_some()); // should get a error response let pending = nostr_manager.get_pending_nwc_invoices().unwrap(); @@ -1439,7 +1445,7 @@ mod wasm_test { let (invoice, _) = create_dummy_invoice(Some(budget + 1), Network::Regtest, None); let event = create_nwc_request(&uri, invoice.to_string()); let result = nwc - .handle_nwc_request(event.clone(), &node, &nostr_manager) + .handle_nwc_request(event.clone(), &mw, &nostr_manager) .await; assert!(result.unwrap().is_some()); // should get a error response let pending = nostr_manager.get_pending_nwc_invoices().unwrap(); @@ -1463,9 +1469,9 @@ mod wasm_test { node.expect_skip_hodl_invoices().once().returning(|| true); node.expect_logger().return_const(MutinyLogger::default()); node.expect_get_outbound_payment_status().return_const(None); - node.expect_pay_invoice_with_timeout() + node.expect_pay_invoice() .once() - .returning(move |inv, _, _, _| { + .returning(move |inv, _, _| { let mut mutiny_invoice: MutinyInvoice = inv.clone().into(); mutiny_invoice.preimage = Some(preimage.to_hex()); mutiny_invoice.status = HTLCStatus::Succeeded; @@ -1475,7 +1481,9 @@ mod wasm_test { }); let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); - let nostr_manager = NostrManager::from_mnemonic(xprivkey, storage.clone(), logger).unwrap(); + let stop = Arc::new(AtomicBool::new(false)); + let nostr_manager = + NostrManager::from_mnemonic(xprivkey, storage.clone(), logger, stop).unwrap(); let budget = 10_000; let profile = nostr_manager diff --git a/mutiny-core/src/test_utils.rs b/mutiny-core/src/test_utils.rs index a33e8d562..4c50354b1 100644 --- a/mutiny-core/src/test_utils.rs +++ b/mutiny-core/src/test_utils.rs @@ -51,6 +51,31 @@ pub fn create_nwc_request(nwc: &NostrWalletConnectURI, invoice: String) -> Event .unwrap() } +pub(crate) async fn create_mutiny_wallet(storage: S) -> MutinyWallet { + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &[0; 32]).unwrap(); + + let config = MutinyWalletConfig::new( + xpriv, + #[cfg(target_arch = "wasm32")] + None, + Network::Regtest, + None, + None, + None, + None, + None, + None, + None, + None, + false, + true, + ); + + MutinyWallet::new(storage.clone(), config, None) + .await + .expect("mutiny wallet should initialize") +} + pub(crate) async fn create_node(storage: S) -> Node { // mark first sync as done so we can execute node functions storage.set_done_first_sync().unwrap(); @@ -115,7 +140,6 @@ pub(crate) async fn create_node(storage: S) -> Node { logger, false, false, - true, #[cfg(target_arch = "wasm32")] String::from("wss://p.mutinywallet.com"), ) @@ -187,8 +211,6 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use uuid::Uuid; -use crate::auth::MutinyAuthClient; -use crate::chain::MutinyChain; use crate::fees::MutinyFeeEstimator; use crate::logging::MutinyLogger; use crate::node::{NetworkGraph, Node, RapidGossipSync}; @@ -198,6 +220,8 @@ use crate::scorer::{HubPreferentialScorer, ProbScorer}; use crate::storage::MutinyStorage; use crate::utils::{now, Mutex}; use crate::vss::MutinyVssClient; +use crate::{auth::MutinyAuthClient, MutinyWallet}; +use crate::{chain::MutinyChain, MutinyWalletConfig}; use crate::{generate_seed, lnurlauth::AuthManager}; pub const MANAGER_BYTES: [u8; 256] = [ diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 6dd017187..d765231ae 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -1480,7 +1480,7 @@ impl MutinyWallet { Ok(self .inner .nostr - .claim_single_use_nwc(amount_sats, &nwc_uri, self.inner.node_manager.as_ref()) + .claim_single_use_nwc(amount_sats, &nwc_uri, &self.inner) .await? .map(|r| r.message)) } @@ -1508,16 +1508,10 @@ impl MutinyWallet { } /// Approves an invoice and sends the payment - pub async fn approve_invoice( - &self, - hash: String, - from_node: String, - ) -> Result<(), MutinyJsError> { - let from_node = PublicKey::from_str(&from_node)?; - + pub async fn approve_invoice(&self, hash: String) -> Result<(), MutinyJsError> { self.inner .nostr - .approve_invoice(hash.parse()?, &self.inner.node_manager, &from_node) + .approve_invoice(hash.parse()?, &self.inner) .await?; Ok(())