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

Commit 9a61026

Browse files
Get estimated fees for channel opens from fedimint
1 parent 622bbf3 commit 9a61026

File tree

4 files changed

+144
-31
lines changed

4 files changed

+144
-31
lines changed

mutiny-core/src/lib.rs

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,13 +1200,11 @@ impl<S: MutinyStorage> MutinyWallet<S> {
12001200
&self,
12011201
amount: Option<u64>,
12021202
) -> Result<FedimintSweepResult, MutinyError> {
1203-
// Attempt to create federation invoice if available and below max amount
1203+
// TODO support more than one federation
12041204
let federation_ids = self.list_federation_ids().await?;
12051205
if federation_ids.is_empty() {
1206-
return Err(MutinyError::BadAmountError);
1206+
return Err(MutinyError::NotFound);
12071207
}
1208-
1209-
// TODO support more than one federation
12101208
let federation_id = &federation_ids[0];
12111209
let federation_lock = self.federations.read().await;
12121210
let fedimint_client = federation_lock
@@ -1215,13 +1213,15 @@ impl<S: MutinyStorage> MutinyWallet<S> {
12151213

12161214
// if the user provided amount, this is easy
12171215
if let Some(amt) = amount {
1218-
let inv = self.node_manager.create_invoice(amt).await?;
1216+
let (inv, fee) = self.node_manager.create_invoice(amt).await?;
12191217
let pay_res = fedimint_client
12201218
.pay_invoice(inv.bolt11.expect("create inv had one job"), vec![])
12211219
.await?;
1220+
let total_fees_paid = pay_res.fees_paid.unwrap_or(0) + fee;
1221+
12221222
return Ok(FedimintSweepResult {
12231223
amount: amt,
1224-
fees: pay_res.fees_paid,
1224+
fees: Some(total_fees_paid),
12251225
});
12261226
}
12271227

@@ -1239,7 +1239,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
12391239
log_debug!(self.logger, "max spendable: {}", amt);
12401240

12411241
// try to get an invoice for this exact amount
1242-
let inv = self.node_manager.create_invoice(amt).await?;
1242+
let (inv, fee) = self.node_manager.create_invoice(amt).await?;
12431243

12441244
// check if we can afford that invoice
12451245
let inv_amt = inv.amount_sats.ok_or(MutinyError::BadAmountError)?;
@@ -1251,23 +1251,18 @@ impl<S: MutinyStorage> MutinyWallet<S> {
12511251
};
12521252

12531253
// if invoice amount changed, create a new invoice
1254-
let inv_to_pay = if first_invoice_amount != inv_amt {
1254+
let (inv_to_pay, fee) = if first_invoice_amount != inv_amt {
12551255
self.node_manager
12561256
.create_invoice(first_invoice_amount)
12571257
.await?
12581258
} else {
1259-
inv.clone()
1259+
(inv.clone(), fee)
12601260
};
12611261

12621262
log_debug!(self.logger, "attempting payment from fedimint client");
1263-
let mut final_result = FedimintSweepResult {
1264-
amount: first_invoice_amount,
1265-
fees: None,
1266-
};
12671263
let first_invoice_res = fedimint_client
12681264
.pay_invoice(inv_to_pay.bolt11.expect("create inv had one job"), vec![])
12691265
.await?;
1270-
final_result.fees = first_invoice_res.fees_paid;
12711266

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

1283-
Ok(final_result)
1278+
Ok(FedimintSweepResult {
1279+
amount: first_invoice_amount,
1280+
fees: Some(first_invoice_res.fees_paid.unwrap_or(0) + fee),
1281+
})
1282+
}
1283+
1284+
/// Estimate the fee before trying to sweep from federation
1285+
pub async fn estimate_sweep_federation_fee(
1286+
&self,
1287+
amount: Option<u64>,
1288+
) -> Result<Option<u64>, MutinyError> {
1289+
if let Some(0) = amount {
1290+
return Ok(None);
1291+
}
1292+
1293+
// TODO support more than one federation
1294+
let federation_ids = self.list_federation_ids().await?;
1295+
if federation_ids.is_empty() {
1296+
return Err(MutinyError::NotFound);
1297+
}
1298+
1299+
let federation_id = &federation_ids[0];
1300+
let federation_lock = self.federations.read().await;
1301+
let fedimint_client = federation_lock
1302+
.get(federation_id)
1303+
.ok_or(MutinyError::NotFound)?;
1304+
1305+
// if the user provided amount, this is easy
1306+
if let Some(amt) = amount {
1307+
return Ok(Some(self.node_manager.get_lsp_fee(amt).await?));
1308+
}
1309+
1310+
// If no amount, figure out the amount to send over
1311+
let current_balance = fedimint_client.get_balance().await?;
1312+
log_debug!(
1313+
self.logger,
1314+
"current fedimint client balance: {}",
1315+
current_balance
1316+
);
1317+
1318+
let fees = fedimint_client.gateway_fee().await?;
1319+
let amt = max_spendable_amount(current_balance, fees)
1320+
.map_or(Err(MutinyError::InsufficientBalance), Ok)?;
1321+
log_debug!(self.logger, "max spendable: {}", amt);
1322+
1323+
// try to get an invoice for this exact amount
1324+
Ok(Some(self.node_manager.get_lsp_fee(amt).await?))
12841325
}
12851326

12861327
async fn create_lightning_invoice(
@@ -1304,7 +1345,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
13041345
}
13051346

13061347
// Fallback to node_manager invoice creation if no federation invoice created
1307-
let inv = self.node_manager.create_invoice(amount).await?;
1348+
let (inv, _fee) = self.node_manager.create_invoice(amount).await?;
13081349
self.storage
13091350
.set_invoice_labels(inv.bolt11.clone().expect("just created"), labels)?;
13101351
Ok(inv)

mutiny-core/src/node.rs

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -968,12 +968,64 @@ impl<S: MutinyStorage> Node<S> {
968968
pub fn get_phantom_route_hint(&self) -> PhantomRouteHints {
969969
self.channel_manager.get_phantom_route_hints()
970970
}
971+
pub async fn get_lsp_fee(&self, amount_sat: u64) -> Result<u64, MutinyError> {
972+
match self.lsp_client.as_ref() {
973+
Some(lsp) => {
974+
self.connect_peer(
975+
PubkeyConnectionInfo::new(&lsp.get_lsp_connection_string())?,
976+
None,
977+
)
978+
.await?;
979+
980+
// Needs any amount over 0 if channel exists
981+
// Needs amount over minimum if no channel
982+
let inbound_capacity_msat: u64 = self
983+
.channel_manager
984+
.list_channels_with_counterparty(&lsp.get_lsp_pubkey())
985+
.iter()
986+
.map(|c| c.inbound_capacity_msat)
987+
.sum();
988+
989+
log_debug!(self.logger, "Current inbound liquidity {inbound_capacity_msat}msats, creating invoice for {}msats", amount_sat * 1000);
990+
991+
let has_inbound_capacity = inbound_capacity_msat > amount_sat * 1_000;
992+
993+
let min_amount_sat = if has_inbound_capacity {
994+
1
995+
} else {
996+
utils::min_lightning_amount(self.network)
997+
};
998+
999+
if amount_sat < min_amount_sat {
1000+
return Err(MutinyError::BadAmountError);
1001+
}
1002+
1003+
let user_channel_id = match lsp {
1004+
AnyLsp::VoltageFlow(_) => None,
1005+
AnyLsp::Lsps(_) => Some(utils::now().as_secs().into()),
1006+
};
1007+
1008+
// check the fee from the LSP
1009+
let lsp_fee = lsp
1010+
.get_lsp_fee_msat(FeeRequest {
1011+
pubkey: self.pubkey.to_hex(),
1012+
amount_msat: amount_sat * 1000,
1013+
user_channel_id,
1014+
})
1015+
.await?;
1016+
1017+
// Convert the fee from msat to sat for comparison and subtraction
1018+
Ok(lsp_fee.fee_amount_msat / 1000)
1019+
}
1020+
None => Ok(0),
1021+
}
1022+
}
9711023

9721024
pub async fn create_invoice(
9731025
&self,
9741026
amount_sat: u64,
9751027
route_hints: Option<Vec<PhantomRouteHints>>,
976-
) -> Result<Bolt11Invoice, MutinyError> {
1028+
) -> Result<(Bolt11Invoice, u64), MutinyError> {
9771029
match self.lsp_client.as_ref() {
9781030
Some(lsp) => {
9791031
self.connect_peer(
@@ -1072,13 +1124,15 @@ impl<S: MutinyStorage> Node<S> {
10721124

10731125
log_debug!(self.logger, "Got wrapped invoice from LSP: {lsp_invoice}");
10741126

1075-
Ok(lsp_invoice)
1127+
Ok((lsp_invoice, lsp_fee_sat))
10761128
}
10771129
AnyLsp::Lsps(client) => {
10781130
if has_inbound_capacity {
1079-
Ok(self
1080-
.create_internal_invoice(Some(amount_sat), None, route_hints)
1081-
.await?)
1131+
Ok((
1132+
self.create_internal_invoice(Some(amount_sat), None, route_hints)
1133+
.await?,
1134+
0,
1135+
))
10821136
} else {
10831137
let lsp_invoice = match client
10841138
.get_lsp_invoice(InvoiceRequest {
@@ -1103,14 +1157,16 @@ impl<S: MutinyStorage> Node<S> {
11031157
return Err(e);
11041158
}
11051159
};
1106-
Ok(lsp_invoice)
1160+
Ok((lsp_invoice, 0))
11071161
}
11081162
}
11091163
}
11101164
}
1111-
None => Ok(self
1112-
.create_internal_invoice(Some(amount_sat), None, route_hints)
1113-
.await?),
1165+
None => Ok((
1166+
self.create_internal_invoice(Some(amount_sat), None, route_hints)
1167+
.await?,
1168+
0,
1169+
)),
11141170
}
11151171
}
11161172

@@ -2417,7 +2473,7 @@ mod tests {
24172473

24182474
let amount_sats = 1_000;
24192475

2420-
let invoice = node.create_invoice(amount_sats, None).await.unwrap();
2476+
let invoice = node.create_invoice(amount_sats, None).await.unwrap().0;
24212477

24222478
assert_eq!(invoice.amount_milli_satoshis(), Some(amount_sats * 1000));
24232479
match invoice.description() {
@@ -2448,7 +2504,7 @@ mod tests {
24482504
let storage = MemoryStorage::default();
24492505
let node = create_node(storage).await;
24502506

2451-
let invoice = node.create_invoice(10_000, None).await.unwrap();
2507+
let invoice = node.create_invoice(10_000, None).await.unwrap().0;
24522508

24532509
let result = node
24542510
.pay_invoice_with_timeout(&invoice, None, None, vec![])
@@ -2571,7 +2627,7 @@ mod wasm_test {
25712627

25722628
let amount_sats = 1_000;
25732629

2574-
let invoice = node.create_invoice(amount_sats, None).await.unwrap();
2630+
let invoice = node.create_invoice(amount_sats, None).await.unwrap().0;
25752631

25762632
assert_eq!(invoice.amount_milli_satoshis(), Some(amount_sats * 1000));
25772633
match invoice.description() {
@@ -2602,7 +2658,7 @@ mod wasm_test {
26022658
let storage = MemoryStorage::default();
26032659
let node = create_node(storage).await;
26042660

2605-
let invoice = node.create_invoice(10_000, None).await.unwrap();
2661+
let invoice = node.create_invoice(10_000, None).await.unwrap().0;
26062662

26072663
let result = node
26082664
.pay_invoice_with_timeout(&invoice, None, None, vec![])

mutiny-core/src/nodemanager.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,7 +1369,7 @@ impl<S: MutinyStorage> NodeManager<S> {
13691369
///
13701370
/// If the manager has more than one node it will create a phantom invoice.
13711371
/// If there is only one node it will create an invoice just for that node.
1372-
pub async fn create_invoice(&self, amount: u64) -> Result<MutinyInvoice, MutinyError> {
1372+
pub async fn create_invoice(&self, amount: u64) -> Result<(MutinyInvoice, u64), MutinyError> {
13731373
let nodes = self.nodes.lock().await;
13741374
let use_phantom = nodes.len() > 1 && self.lsp_config.is_none();
13751375
if nodes.len() == 0 {
@@ -1394,7 +1394,15 @@ impl<S: MutinyStorage> NodeManager<S> {
13941394
};
13951395
let invoice = first_node.create_invoice(amount, route_hints).await?;
13961396

1397-
Ok(invoice.into())
1397+
Ok((invoice.0.into(), invoice.1))
1398+
}
1399+
1400+
/// Gets the LSP fee for receiving an invoice down the first node that exists.
1401+
/// This could include the fee if a channel open is necessary. Otherwise the fee
1402+
/// will be low or non-existant.
1403+
pub async fn get_lsp_fee(&self, amount: u64) -> Result<u64, MutinyError> {
1404+
let node = self.get_node_by_key_or_first(None).await?;
1405+
node.get_lsp_fee(amount).await
13981406
}
13991407

14001408
/// Pays a lightning invoice from either a specified node or the first available node.

mutiny-wasm/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,14 @@ impl MutinyWallet {
10041004
Ok(self.inner.sweep_federation_balance(amount).await?.into())
10051005
}
10061006

1007+
/// Estimate the fee before trying to sweep from federation
1008+
pub async fn estimate_sweep_federation_fee(
1009+
&self,
1010+
amount: Option<u64>,
1011+
) -> Result<Option<u64>, MutinyJsError> {
1012+
Ok(self.inner.estimate_sweep_federation_fee(amount).await?)
1013+
}
1014+
10071015
/// Closes a channel with the given outpoint.
10081016
///
10091017
/// If force is true, the channel will be force closed.

0 commit comments

Comments
 (0)