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

Commit

Permalink
Swap fedimint balance to lightning channel
Browse files Browse the repository at this point in the history
  • Loading branch information
AnthonyRonning committed Jan 31, 2024
1 parent 82fff50 commit 848c3f3
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 5 deletions.
234 changes: 229 additions & 5 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,18 @@ mod test_utils;
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::storage::{
list_payment_info, 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::{
event::{HTLCStatus, MillisatAmount, PaymentInfo},
onchain::FULL_SYNC_STOP_GAP,
};
use crate::{
federation::GatewayFees,
storage::{
list_payment_info, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY,
},
};
use crate::{
federation::{FederationClient, FederationIdentity, FederationIndex, FederationStorage},
labels::{get_contact_key, Contact, LabelStorage},
Expand Down Expand Up @@ -1122,6 +1125,94 @@ impl<S: MutinyStorage> MutinyWallet<S> {
})
}

pub async fn sweep_federation_balance(&self, amount: Option<u64>) -> Result<(), MutinyError> {
// Attempt to create federation invoice if available and below max amount
let federation_ids = self.list_federation_ids().await?;
if federation_ids.is_empty() {
return Err(MutinyError::BadAmountError);
}

// TODO support more than one federation
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 {
let inv = self.node_manager.create_invoice(amt).await?;
let _ = fedimint_client
.pay_invoice(inv.bolt11.expect("create inv had one job"), vec![])
.await?;
return Ok(());
}

// If no amount, figure out the amount to send over
let current_balance = fedimint_client.get_balance().await?;
log_info!(
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_info!(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_amt = inv.amount_sats.ok_or(MutinyError::BadAmountError)?;
let inv_to_pay = if inv_amt > amt {
let new_amt = inv_amt - (inv_amt - amt);
log_info!(self.logger, "adjusting amount to swap to: {}", amt);
self.node_manager.create_invoice(new_amt).await?
} else {
inv.clone()
};

log_info!(self.logger, "attempting payment from fedimint client");
let _ = fedimint_client
.pay_invoice(inv_to_pay.bolt11.expect("create inv had one job"), vec![])
.await?;

// pay_invoice returns invoice if Succeeded or Err if something else
// it's safe to assume that it went through and we can check remaining balance
let remaining_balance = fedimint_client.get_balance().await?;
log_info!(
self.logger,
"remaining fedimint balance: {}",
remaining_balance
);
if remaining_balance != 0 {
// the fee for existing channel is voltage 1 sat + base fee + ppm
let remaining_balance_minus_fee = remaining_balance - 1; // Voltage is 1 sat fee

let inv = self
.node_manager
.create_invoice(remaining_balance_minus_fee)
.await?;

match fedimint_client
.pay_invoice(inv.bolt11.expect("create inv had one job"), vec![])
.await
{
Ok(_) => {
log_info!(self.logger, "paid remaining balance")
}
Err(e) => {
// Don't want to return this error since it's just "incomplete",
// and just not the full amount.
log_warn!(self.logger, "error paying remaining balance: {}", e)
}
}
}

Ok(())
}

async fn create_lightning_invoice(
&self,
amount: u64,
Expand Down Expand Up @@ -2089,11 +2180,139 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
})
}

// max amount that can be spent through a gateway
fn max_spendable_amount(amount_sat: u64, routing_fees: GatewayFees) -> Option<u64> {
let amount_msat = amount_sat as f64 * 1_000.0;

let prop_fee_msat = (amount_msat * routing_fees.proportional_millionths as f64) / 1_000_000.0;

let initial_max = amount_msat - (routing_fees.base_msat as f64 + prop_fee_msat);

if initial_max <= 0.0 {
return None;
}

if amount_msat - initial_max < 1.0 {
return Some((initial_max / 1_000.0).floor() as u64);
}

let mut new_max = initial_max;
while new_max < amount_msat {
let new_check = new_max + 1.0;

let prop_fee_sat = (new_check * routing_fees.proportional_millionths as f64) / 1_000_000.0;

let new_amt = new_check + routing_fees.base_msat as f64 + prop_fee_sat;

if amount_msat - new_amt <= 0.0 {
// overshot it
return Some((new_max / 1_000.0).floor() as u64);
}

new_max += 1.0;
}

Some((new_max / 1_000.0).floor() as u64)
}

#[cfg(test)]
fn max_routing_fee_amount() {
let initial_budget = 1;
let routing_fees = GatewayFees {
base_msat: 10_000,
proportional_millionths: 0,
};
assert_eq!(None, max_spendable_amount(initial_budget, routing_fees));

// only a percentage fee
let initial_budget = 100;
let routing_fees = GatewayFees {
base_msat: 0,
proportional_millionths: 0,
};
assert_eq!(
Some(100),
max_spendable_amount(initial_budget, routing_fees)
);

let initial_budget = 100;
let routing_fees = GatewayFees {
base_msat: 0,
proportional_millionths: 10_000,
};
assert_eq!(Some(99), max_spendable_amount(initial_budget, routing_fees));

let initial_budget = 100;
let routing_fees = GatewayFees {
base_msat: 0,
proportional_millionths: 100_000,
};
assert_eq!(Some(90), max_spendable_amount(initial_budget, routing_fees));

let initial_budget = 101_000;
let routing_fees = GatewayFees {
base_msat: 0,
proportional_millionths: 100_000,
};
assert_eq!(
Some(91_818),
max_spendable_amount(initial_budget, routing_fees)
);

let initial_budget = 101;
let routing_fees = GatewayFees {
base_msat: 0,
proportional_millionths: 100_000,
};
assert_eq!(Some(91), max_spendable_amount(initial_budget, routing_fees));

// same tests but with a base fee
let initial_budget = 100;
let routing_fees = GatewayFees {
base_msat: 1_000,
proportional_millionths: 0,
};
assert_eq!(Some(99), max_spendable_amount(initial_budget, routing_fees));

let initial_budget = 100;
let routing_fees = GatewayFees {
base_msat: 1_000,
proportional_millionths: 10_000,
};
assert_eq!(Some(98), max_spendable_amount(initial_budget, routing_fees));

let initial_budget = 100;
let routing_fees = GatewayFees {
base_msat: 1_000,
proportional_millionths: 100_000,
};
assert_eq!(Some(89), max_spendable_amount(initial_budget, routing_fees));

let initial_budget = 101;
let routing_fees = GatewayFees {
base_msat: 1_000,
proportional_millionths: 100_000,
};
assert_eq!(Some(90), max_spendable_amount(initial_budget, routing_fees));
}

#[cfg(test)]
#[cfg(not(target_arch = "wasm32"))]
mod tests {
use super::*;

#[test]
fn test_max_routing_fee_amount() {
max_routing_fee_amount();
}
}

#[cfg(test)]
#[cfg(target_arch = "wasm32")]
mod tests {
use crate::{
encrypt::encryption_key_from_pass, generate_seed, nodemanager::NodeManager, MutinyWallet,
MutinyWalletBuilder, MutinyWalletConfigBuilder,
encrypt::encryption_key_from_pass, generate_seed, max_routing_fee_amount,
nodemanager::NodeManager, MutinyWallet, MutinyWalletBuilder, MutinyWalletConfigBuilder,
};
use bitcoin::util::bip32::ExtendedPrivKey;
use bitcoin::Network;
Expand Down Expand Up @@ -2401,4 +2620,9 @@ mod tests {
assert_eq!(next.len(), 2);
assert!(next.iter().all(|m| !messages.contains(m)))
}

#[test]
fn test_max_routing_fee_amount() {
max_routing_fee_amount();
}
}
5 changes: 5 additions & 0 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,11 @@ impl MutinyWallet {
.into())
}

/// Sweep the federation balance into a lightning channel
pub async fn sweep_federation_balance(&self, amount: Option<u64>) -> Result<(), MutinyJsError> {
Ok(self.inner.sweep_federation_balance(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 848c3f3

Please sign in to comment.