From 274756485434982dca39443501957b5b21c235a6 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Thu, 1 Feb 2024 11:11:37 -0600 Subject: [PATCH] Get estimated fees for channel opens from fedimint --- mutiny-core/src/lib.rs | 73 +++++++++++++++++++++++------- mutiny-core/src/node.rs | 82 ++++++++++++++++++++++++++++------ mutiny-core/src/nodemanager.rs | 12 ++++- mutiny-wasm/src/lib.rs | 8 ++++ 4 files changed, 144 insertions(+), 31 deletions(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 9aaa5a487..11ac3b976 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -1193,13 +1193,11 @@ impl MutinyWallet { &self, amount: Option, ) -> Result { - // Attempt to create federation invoice if available and below max amount + // TODO support more than one federation let federation_ids = self.list_federation_ids().await?; if federation_ids.is_empty() { - return Err(MutinyError::BadAmountError); + return Err(MutinyError::NotFound); } - - // TODO support more than one federation let federation_id = &federation_ids[0]; let federation_lock = self.federations.read().await; let fedimint_client = federation_lock @@ -1208,7 +1206,7 @@ impl MutinyWallet { // if the user provided amount, this is easy if let Some(amt) = amount { - let inv = self.node_manager.create_invoice(amt).await?; + let (inv, fee) = self.node_manager.create_invoice(amt).await?; self.storage.set_invoice_labels( inv.bolt11.clone().expect("just created"), vec![SWAP_LABEL.to_string()], @@ -1219,9 +1217,11 @@ impl MutinyWallet { vec![SWAP_LABEL.to_string()], ) .await?; + let total_fees_paid = pay_res.fees_paid.unwrap_or(0) + fee; + return Ok(FedimintSweepResult { amount: amt, - fees: pay_res.fees_paid, + fees: Some(total_fees_paid), }); } @@ -1239,7 +1239,7 @@ impl MutinyWallet { log_debug!(self.logger, "max spendable: {}", amt); // try to get an invoice for this exact amount - let inv = self.node_manager.create_invoice(amt).await?; + let (inv, fee) = self.node_manager.create_invoice(amt).await?; // check if we can afford that invoice let inv_amt = inv.amount_sats.ok_or(MutinyError::BadAmountError)?; @@ -1251,12 +1251,12 @@ impl MutinyWallet { }; // if invoice amount changed, create a new invoice - let inv_to_pay = if first_invoice_amount != inv_amt { + let (inv_to_pay, fee) = if first_invoice_amount != inv_amt { self.node_manager .create_invoice(first_invoice_amount) .await? } else { - inv.clone() + (inv.clone(), fee) }; self.storage.set_invoice_labels( inv_to_pay.bolt11.clone().expect("just created"), @@ -1264,17 +1264,12 @@ impl MutinyWallet { )?; log_debug!(self.logger, "attempting payment from fedimint client"); - let mut final_result = FedimintSweepResult { - amount: first_invoice_amount, - fees: None, - }; let first_invoice_res = fedimint_client .pay_invoice( inv_to_pay.bolt11.expect("create inv had one job"), vec![SWAP_LABEL.to_string()], ) .await?; - final_result.fees = first_invoice_res.fees_paid; let remaining_balance = fedimint_client.get_balance().await?; if remaining_balance > 0 { @@ -1287,7 +1282,53 @@ impl MutinyWallet { ); } - Ok(final_result) + Ok(FedimintSweepResult { + amount: first_invoice_amount, + fees: Some(first_invoice_res.fees_paid.unwrap_or(0) + fee), + }) + } + + /// Estimate the fee before trying to sweep from federation + pub async fn estimate_sweep_federation_fee( + &self, + amount: Option, + ) -> Result, MutinyError> { + if let Some(0) = amount { + return Ok(None); + } + + // TODO support more than one federation + let federation_ids = self.list_federation_ids().await?; + if federation_ids.is_empty() { + return Err(MutinyError::NotFound); + } + + let federation_id = &federation_ids[0]; + let federation_lock = self.federations.read().await; + let fedimint_client = federation_lock + .get(federation_id) + .ok_or(MutinyError::NotFound)?; + + // if the user provided amount, this is easy + if let Some(amt) = amount { + return Ok(Some(self.node_manager.get_lsp_fee(amt).await?)); + } + + // If no amount, figure out the amount to send over + let current_balance = fedimint_client.get_balance().await?; + log_debug!( + self.logger, + "current fedimint client balance: {}", + current_balance + ); + + let fees = fedimint_client.gateway_fee().await?; + let amt = max_spendable_amount(current_balance, &fees) + .map_or(Err(MutinyError::InsufficientBalance), Ok)?; + log_debug!(self.logger, "max spendable: {}", amt); + + // try to get an invoice for this exact amount + Ok(Some(self.node_manager.get_lsp_fee(amt).await?)) } async fn create_lightning_invoice( @@ -1311,7 +1352,7 @@ impl MutinyWallet { } // Fallback to node_manager invoice creation if no federation invoice created - let inv = self.node_manager.create_invoice(amount).await?; + let (inv, _fee) = self.node_manager.create_invoice(amount).await?; self.storage .set_invoice_labels(inv.bolt11.clone().expect("just created"), labels)?; Ok(inv) diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index f8c2d2818..876fb289a 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -968,12 +968,64 @@ impl Node { pub fn get_phantom_route_hint(&self) -> PhantomRouteHints { self.channel_manager.get_phantom_route_hints() } + pub async fn get_lsp_fee(&self, amount_sat: u64) -> Result { + match self.lsp_client.as_ref() { + Some(lsp) => { + self.connect_peer( + PubkeyConnectionInfo::new(&lsp.get_lsp_connection_string())?, + None, + ) + .await?; + + // Needs any amount over 0 if channel exists + // Needs amount over minimum if no channel + let inbound_capacity_msat: u64 = self + .channel_manager + .list_channels_with_counterparty(&lsp.get_lsp_pubkey()) + .iter() + .map(|c| c.inbound_capacity_msat) + .sum(); + + log_debug!(self.logger, "Current inbound liquidity {inbound_capacity_msat}msats, creating invoice for {}msats", amount_sat * 1000); + + let has_inbound_capacity = inbound_capacity_msat > amount_sat * 1_000; + + let min_amount_sat = if has_inbound_capacity { + 1 + } else { + utils::min_lightning_amount(self.network) + }; + + if amount_sat < min_amount_sat { + return Err(MutinyError::BadAmountError); + } + + let user_channel_id = match lsp { + AnyLsp::VoltageFlow(_) => None, + AnyLsp::Lsps(_) => Some(utils::now().as_secs().into()), + }; + + // check the fee from the LSP + let lsp_fee = lsp + .get_lsp_fee_msat(FeeRequest { + pubkey: self.pubkey.to_hex(), + amount_msat: amount_sat * 1000, + user_channel_id, + }) + .await?; + + // Convert the fee from msat to sat for comparison and subtraction + Ok(lsp_fee.fee_amount_msat / 1000) + } + None => Ok(0), + } + } pub async fn create_invoice( &self, amount_sat: u64, route_hints: Option>, - ) -> Result { + ) -> Result<(Bolt11Invoice, u64), MutinyError> { match self.lsp_client.as_ref() { Some(lsp) => { self.connect_peer( @@ -1072,13 +1124,15 @@ impl Node { log_debug!(self.logger, "Got wrapped invoice from LSP: {lsp_invoice}"); - Ok(lsp_invoice) + Ok((lsp_invoice, lsp_fee_sat)) } AnyLsp::Lsps(client) => { if has_inbound_capacity { - Ok(self - .create_internal_invoice(Some(amount_sat), None, route_hints) - .await?) + Ok(( + self.create_internal_invoice(Some(amount_sat), None, route_hints) + .await?, + 0, + )) } else { let lsp_invoice = match client .get_lsp_invoice(InvoiceRequest { @@ -1103,14 +1157,16 @@ impl Node { return Err(e); } }; - Ok(lsp_invoice) + Ok((lsp_invoice, 0)) } } } } - None => Ok(self - .create_internal_invoice(Some(amount_sat), None, route_hints) - .await?), + None => Ok(( + self.create_internal_invoice(Some(amount_sat), None, route_hints) + .await?, + 0, + )), } } @@ -2417,7 +2473,7 @@ mod tests { let amount_sats = 1_000; - let invoice = node.create_invoice(amount_sats, None).await.unwrap(); + let invoice = node.create_invoice(amount_sats, None).await.unwrap().0; assert_eq!(invoice.amount_milli_satoshis(), Some(amount_sats * 1000)); match invoice.description() { @@ -2448,7 +2504,7 @@ mod tests { let storage = MemoryStorage::default(); let node = create_node(storage).await; - let invoice = node.create_invoice(10_000, None).await.unwrap(); + let invoice = node.create_invoice(10_000, None).await.unwrap().0; let result = node .pay_invoice_with_timeout(&invoice, None, None, vec![]) @@ -2571,7 +2627,7 @@ mod wasm_test { let amount_sats = 1_000; - let invoice = node.create_invoice(amount_sats, None).await.unwrap(); + let invoice = node.create_invoice(amount_sats, None).await.unwrap().0; assert_eq!(invoice.amount_milli_satoshis(), Some(amount_sats * 1000)); match invoice.description() { @@ -2602,7 +2658,7 @@ mod wasm_test { let storage = MemoryStorage::default(); let node = create_node(storage).await; - let invoice = node.create_invoice(10_000, None).await.unwrap(); + let invoice = node.create_invoice(10_000, None).await.unwrap().0; let result = node .pay_invoice_with_timeout(&invoice, None, None, vec![]) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 7dd0411cf..b8db0942c 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -1369,7 +1369,7 @@ impl NodeManager { /// /// If the manager has more than one node it will create a phantom invoice. /// If there is only one node it will create an invoice just for that node. - pub async fn create_invoice(&self, amount: u64) -> Result { + pub async fn create_invoice(&self, amount: u64) -> Result<(MutinyInvoice, u64), MutinyError> { let nodes = self.nodes.lock().await; let use_phantom = nodes.len() > 1 && self.lsp_config.is_none(); if nodes.len() == 0 { @@ -1394,7 +1394,15 @@ impl NodeManager { }; let invoice = first_node.create_invoice(amount, route_hints).await?; - Ok(invoice.into()) + Ok((invoice.0.into(), invoice.1)) + } + + /// Gets the LSP fee for receiving an invoice down the first node that exists. + /// This could include the fee if a channel open is necessary. Otherwise the fee + /// will be low or non-existant. + pub async fn get_lsp_fee(&self, amount: u64) -> Result { + let node = self.get_node_by_key_or_first(None).await?; + node.get_lsp_fee(amount).await } /// Pays a lightning invoice from either a specified node or the first available node. diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 662094ac2..bcf5cbfa3 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -1004,6 +1004,14 @@ impl MutinyWallet { Ok(self.inner.sweep_federation_balance(amount).await?.into()) } + /// Estimate the fee before trying to sweep from federation + pub async fn estimate_sweep_federation_fee( + &self, + amount: Option, + ) -> Result, MutinyJsError> { + Ok(self.inner.estimate_sweep_federation_fee(amount).await?) + } + /// Closes a channel with the given outpoint. /// /// If force is true, the channel will be force closed.