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

Commit 848c3f3

Browse files
Swap fedimint balance to lightning channel
1 parent 82fff50 commit 848c3f3

File tree

2 files changed

+234
-5
lines changed

2 files changed

+234
-5
lines changed

mutiny-core/src/lib.rs

Lines changed: 229 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,18 @@ mod test_utils;
4343
pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY};
4444
pub use crate::keymanager::generate_seed;
4545
pub use crate::ldkstorage::{CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY};
46-
use crate::storage::{
47-
list_payment_info, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY,
48-
};
4946
use crate::{auth::MutinyAuthClient, logging::MutinyLogger};
5047
use crate::{error::MutinyError, nostr::ReservedProfile};
5148
use crate::{
5249
event::{HTLCStatus, MillisatAmount, PaymentInfo},
5350
onchain::FULL_SYNC_STOP_GAP,
5451
};
52+
use crate::{
53+
federation::GatewayFees,
54+
storage::{
55+
list_payment_info, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY,
56+
},
57+
};
5558
use crate::{
5659
federation::{FederationClient, FederationIdentity, FederationIndex, FederationStorage},
5760
labels::{get_contact_key, Contact, LabelStorage},
@@ -1122,6 +1125,94 @@ impl<S: MutinyStorage> MutinyWallet<S> {
11221125
})
11231126
}
11241127

1128+
pub async fn sweep_federation_balance(&self, amount: Option<u64>) -> Result<(), MutinyError> {
1129+
// Attempt to create federation invoice if available and below max amount
1130+
let federation_ids = self.list_federation_ids().await?;
1131+
if federation_ids.is_empty() {
1132+
return Err(MutinyError::BadAmountError);
1133+
}
1134+
1135+
// TODO support more than one federation
1136+
let federation_id = &federation_ids[0];
1137+
let federation_lock = self.federations.read().await;
1138+
let fedimint_client = federation_lock
1139+
.get(federation_id)
1140+
.ok_or(MutinyError::NotFound)?;
1141+
1142+
// if the user provided amount, this is easy
1143+
if let Some(amt) = amount {
1144+
let inv = self.node_manager.create_invoice(amt).await?;
1145+
let _ = fedimint_client
1146+
.pay_invoice(inv.bolt11.expect("create inv had one job"), vec![])
1147+
.await?;
1148+
return Ok(());
1149+
}
1150+
1151+
// If no amount, figure out the amount to send over
1152+
let current_balance = fedimint_client.get_balance().await?;
1153+
log_info!(
1154+
self.logger,
1155+
"current fedimint client balance: {}",
1156+
current_balance
1157+
);
1158+
1159+
let fees = fedimint_client.gateway_fee().await?;
1160+
let amt = max_spendable_amount(current_balance, fees)
1161+
.map_or(Err(MutinyError::InsufficientBalance), Ok)?;
1162+
log_info!(self.logger, "max spendable: {}", amt);
1163+
1164+
// try to get an invoice for this exact amount
1165+
let inv = self.node_manager.create_invoice(amt).await?;
1166+
1167+
let inv_amt = inv.amount_sats.ok_or(MutinyError::BadAmountError)?;
1168+
let inv_to_pay = if inv_amt > amt {
1169+
let new_amt = inv_amt - (inv_amt - amt);
1170+
log_info!(self.logger, "adjusting amount to swap to: {}", amt);
1171+
self.node_manager.create_invoice(new_amt).await?
1172+
} else {
1173+
inv.clone()
1174+
};
1175+
1176+
log_info!(self.logger, "attempting payment from fedimint client");
1177+
let _ = fedimint_client
1178+
.pay_invoice(inv_to_pay.bolt11.expect("create inv had one job"), vec![])
1179+
.await?;
1180+
1181+
// pay_invoice returns invoice if Succeeded or Err if something else
1182+
// it's safe to assume that it went through and we can check remaining balance
1183+
let remaining_balance = fedimint_client.get_balance().await?;
1184+
log_info!(
1185+
self.logger,
1186+
"remaining fedimint balance: {}",
1187+
remaining_balance
1188+
);
1189+
if remaining_balance != 0 {
1190+
// the fee for existing channel is voltage 1 sat + base fee + ppm
1191+
let remaining_balance_minus_fee = remaining_balance - 1; // Voltage is 1 sat fee
1192+
1193+
let inv = self
1194+
.node_manager
1195+
.create_invoice(remaining_balance_minus_fee)
1196+
.await?;
1197+
1198+
match fedimint_client
1199+
.pay_invoice(inv.bolt11.expect("create inv had one job"), vec![])
1200+
.await
1201+
{
1202+
Ok(_) => {
1203+
log_info!(self.logger, "paid remaining balance")
1204+
}
1205+
Err(e) => {
1206+
// Don't want to return this error since it's just "incomplete",
1207+
// and just not the full amount.
1208+
log_warn!(self.logger, "error paying remaining balance: {}", e)
1209+
}
1210+
}
1211+
}
1212+
1213+
Ok(())
1214+
}
1215+
11251216
async fn create_lightning_invoice(
11261217
&self,
11271218
amount: u64,
@@ -2089,11 +2180,139 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
20892180
})
20902181
}
20912182

2183+
// max amount that can be spent through a gateway
2184+
fn max_spendable_amount(amount_sat: u64, routing_fees: GatewayFees) -> Option<u64> {
2185+
let amount_msat = amount_sat as f64 * 1_000.0;
2186+
2187+
let prop_fee_msat = (amount_msat * routing_fees.proportional_millionths as f64) / 1_000_000.0;
2188+
2189+
let initial_max = amount_msat - (routing_fees.base_msat as f64 + prop_fee_msat);
2190+
2191+
if initial_max <= 0.0 {
2192+
return None;
2193+
}
2194+
2195+
if amount_msat - initial_max < 1.0 {
2196+
return Some((initial_max / 1_000.0).floor() as u64);
2197+
}
2198+
2199+
let mut new_max = initial_max;
2200+
while new_max < amount_msat {
2201+
let new_check = new_max + 1.0;
2202+
2203+
let prop_fee_sat = (new_check * routing_fees.proportional_millionths as f64) / 1_000_000.0;
2204+
2205+
let new_amt = new_check + routing_fees.base_msat as f64 + prop_fee_sat;
2206+
2207+
if amount_msat - new_amt <= 0.0 {
2208+
// overshot it
2209+
return Some((new_max / 1_000.0).floor() as u64);
2210+
}
2211+
2212+
new_max += 1.0;
2213+
}
2214+
2215+
Some((new_max / 1_000.0).floor() as u64)
2216+
}
2217+
20922218
#[cfg(test)]
2219+
fn max_routing_fee_amount() {
2220+
let initial_budget = 1;
2221+
let routing_fees = GatewayFees {
2222+
base_msat: 10_000,
2223+
proportional_millionths: 0,
2224+
};
2225+
assert_eq!(None, max_spendable_amount(initial_budget, routing_fees));
2226+
2227+
// only a percentage fee
2228+
let initial_budget = 100;
2229+
let routing_fees = GatewayFees {
2230+
base_msat: 0,
2231+
proportional_millionths: 0,
2232+
};
2233+
assert_eq!(
2234+
Some(100),
2235+
max_spendable_amount(initial_budget, routing_fees)
2236+
);
2237+
2238+
let initial_budget = 100;
2239+
let routing_fees = GatewayFees {
2240+
base_msat: 0,
2241+
proportional_millionths: 10_000,
2242+
};
2243+
assert_eq!(Some(99), max_spendable_amount(initial_budget, routing_fees));
2244+
2245+
let initial_budget = 100;
2246+
let routing_fees = GatewayFees {
2247+
base_msat: 0,
2248+
proportional_millionths: 100_000,
2249+
};
2250+
assert_eq!(Some(90), max_spendable_amount(initial_budget, routing_fees));
2251+
2252+
let initial_budget = 101_000;
2253+
let routing_fees = GatewayFees {
2254+
base_msat: 0,
2255+
proportional_millionths: 100_000,
2256+
};
2257+
assert_eq!(
2258+
Some(91_818),
2259+
max_spendable_amount(initial_budget, routing_fees)
2260+
);
2261+
2262+
let initial_budget = 101;
2263+
let routing_fees = GatewayFees {
2264+
base_msat: 0,
2265+
proportional_millionths: 100_000,
2266+
};
2267+
assert_eq!(Some(91), max_spendable_amount(initial_budget, routing_fees));
2268+
2269+
// same tests but with a base fee
2270+
let initial_budget = 100;
2271+
let routing_fees = GatewayFees {
2272+
base_msat: 1_000,
2273+
proportional_millionths: 0,
2274+
};
2275+
assert_eq!(Some(99), max_spendable_amount(initial_budget, routing_fees));
2276+
2277+
let initial_budget = 100;
2278+
let routing_fees = GatewayFees {
2279+
base_msat: 1_000,
2280+
proportional_millionths: 10_000,
2281+
};
2282+
assert_eq!(Some(98), max_spendable_amount(initial_budget, routing_fees));
2283+
2284+
let initial_budget = 100;
2285+
let routing_fees = GatewayFees {
2286+
base_msat: 1_000,
2287+
proportional_millionths: 100_000,
2288+
};
2289+
assert_eq!(Some(89), max_spendable_amount(initial_budget, routing_fees));
2290+
2291+
let initial_budget = 101;
2292+
let routing_fees = GatewayFees {
2293+
base_msat: 1_000,
2294+
proportional_millionths: 100_000,
2295+
};
2296+
assert_eq!(Some(90), max_spendable_amount(initial_budget, routing_fees));
2297+
}
2298+
2299+
#[cfg(test)]
2300+
#[cfg(not(target_arch = "wasm32"))]
2301+
mod tests {
2302+
use super::*;
2303+
2304+
#[test]
2305+
fn test_max_routing_fee_amount() {
2306+
max_routing_fee_amount();
2307+
}
2308+
}
2309+
2310+
#[cfg(test)]
2311+
#[cfg(target_arch = "wasm32")]
20932312
mod tests {
20942313
use crate::{
2095-
encrypt::encryption_key_from_pass, generate_seed, nodemanager::NodeManager, MutinyWallet,
2096-
MutinyWalletBuilder, MutinyWalletConfigBuilder,
2314+
encrypt::encryption_key_from_pass, generate_seed, max_routing_fee_amount,
2315+
nodemanager::NodeManager, MutinyWallet, MutinyWalletBuilder, MutinyWalletConfigBuilder,
20972316
};
20982317
use bitcoin::util::bip32::ExtendedPrivKey;
20992318
use bitcoin::Network;
@@ -2401,4 +2620,9 @@ mod tests {
24012620
assert_eq!(next.len(), 2);
24022621
assert!(next.iter().all(|m| !messages.contains(m)))
24032622
}
2623+
2624+
#[test]
2625+
fn test_max_routing_fee_amount() {
2626+
max_routing_fee_amount();
2627+
}
24042628
}

mutiny-wasm/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,11 @@ impl MutinyWallet {
976976
.into())
977977
}
978978

979+
/// Sweep the federation balance into a lightning channel
980+
pub async fn sweep_federation_balance(&self, amount: Option<u64>) -> Result<(), MutinyJsError> {
981+
Ok(self.inner.sweep_federation_balance(amount).await?)
982+
}
983+
979984
/// Closes a channel with the given outpoint.
980985
///
981986
/// If force is true, the channel will be force closed.

0 commit comments

Comments
 (0)