Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit

Permalink
Get estimated fees for channel opens from fedimint
Browse files Browse the repository at this point in the history
  • Loading branch information
AnthonyRonning committed Feb 1, 2024
1 parent 622bbf3 commit 9a61026
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 31 deletions.
73 changes: 57 additions & 16 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1200,13 +1200,11 @@ impl<S: MutinyStorage> MutinyWallet<S> {
&self,
amount: Option<u64>,
) -> Result<FedimintSweepResult, MutinyError> {
// 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
Expand All @@ -1215,13 +1213,15 @@ impl<S: MutinyStorage> MutinyWallet<S> {

// 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?;
let pay_res = fedimint_client
.pay_invoice(inv.bolt11.expect("create inv had one job"), vec![])
.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),
});
}

Expand All @@ -1239,7 +1239,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
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)?;
Expand All @@ -1251,23 +1251,18 @@ impl<S: MutinyStorage> MutinyWallet<S> {
};

// 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)
};

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![])
.await?;
final_result.fees = first_invoice_res.fees_paid;

let remaining_balance = fedimint_client.get_balance().await?;
if remaining_balance > 0 {
Expand All @@ -1280,7 +1275,53 @@ impl<S: MutinyStorage> MutinyWallet<S> {
);
}

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<u64>,
) -> Result<Option<u64>, 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(
Expand All @@ -1304,7 +1345,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
}

// 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)
Expand Down
82 changes: 69 additions & 13 deletions mutiny-core/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,12 +968,64 @@ impl<S: MutinyStorage> Node<S> {
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<u64, MutinyError> {
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<Vec<PhantomRouteHints>>,
) -> Result<Bolt11Invoice, MutinyError> {
) -> Result<(Bolt11Invoice, u64), MutinyError> {
match self.lsp_client.as_ref() {
Some(lsp) => {
self.connect_peer(
Expand Down Expand Up @@ -1072,13 +1124,15 @@ impl<S: MutinyStorage> Node<S> {

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 {
Expand All @@ -1103,14 +1157,16 @@ impl<S: MutinyStorage> Node<S> {
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,
)),
}
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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![])
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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![])
Expand Down
12 changes: 10 additions & 2 deletions mutiny-core/src/nodemanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1369,7 +1369,7 @@ impl<S: MutinyStorage> NodeManager<S> {
///
/// 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<MutinyInvoice, MutinyError> {
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 {
Expand All @@ -1394,7 +1394,15 @@ impl<S: MutinyStorage> NodeManager<S> {
};
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<u64, MutinyError> {
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.
Expand Down
8 changes: 8 additions & 0 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>,
) -> Result<Option<u64>, 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.
Expand Down

0 comments on commit 9a61026

Please sign in to comment.