Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow swapping between federations #1170

Merged
merged 3 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 78 additions & 32 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1590,31 +1590,47 @@ impl<S: MutinyStorage> MutinyWallet<S> {
pub async fn sweep_federation_balance(
&self,
amount: Option<u64>,
from_federation_id: Option<FederationId>,
to_federation_id: Option<FederationId>,
) -> Result<FedimintSweepResult, MutinyError> {
// 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 from_federation_id = from_federation_id.unwrap_or(federation_ids[0]);
let federation_lock = self.federations.read().await;
let fedimint_client = federation_lock
.get(federation_id)
let from_fedimint_client = federation_lock
.get(&from_federation_id)
.ok_or(MutinyError::NotFound)?;

// decide to sweep to secondary federation or lightning node
let to_federation_client = match to_federation_id {
Some(f) => Some(federation_lock.get(&f).ok_or(MutinyError::NotFound)?),
None => None,
};

let labels = vec![SWAP_LABEL.to_string()];

// if the user provided amount, this is easy
if let Some(amt) = amount {
let (inv, fee) = self
.node_manager
.create_invoice(amt, labels.clone())
.await?;
let (inv, fee) = match to_federation_client {
Some(f) => {
// swap from one federation to another
let inv = f.get_invoice(amt, labels.clone()).await?;
(inv, 0)
}
None => {
// use the lightning node if no to federation selected
self.node_manager
.create_invoice(amt, labels.clone())
.await?
}
};

let bolt_11 = inv.bolt11.expect("create inv had one job");
self.storage
.set_invoice_labels(bolt_11.clone(), labels.clone())?;
let pay_res = fedimint_client.pay_invoice(bolt_11, labels).await?;
let pay_res = from_fedimint_client.pay_invoice(bolt_11, labels).await?;
let total_fees_paid = pay_res.fees_paid.unwrap_or(0) + fee;

return Ok(FedimintSweepResult {
Expand All @@ -1624,24 +1640,33 @@ impl<S: MutinyStorage> MutinyWallet<S> {
}

// If no amount, figure out the amount to send over
let current_balance = fedimint_client.get_balance().await?;
let current_balance = from_fedimint_client.get_balance().await?;
log_debug!(
self.logger,
"current fedimint client balance: {}",
current_balance
);

let fees = fedimint_client.gateway_fee().await?;
let fees = from_fedimint_client.gateway_fee().await?;
// FIXME: this is still producing off by one. check round down
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
let (inv, fee) = self
.node_manager
.create_invoice(amt, labels.clone())
.await?;
let (inv, fee) = match to_federation_client {
Some(f) => {
// swap from one federation to another
let inv = f.get_invoice(amt, labels.clone()).await?;
(inv, 0)
}
None => {
// use the lightning node if no to federation selected
self.node_manager
.create_invoice(amt, labels.clone())
.await?
}
};

// check if we can afford that invoice
let inv_amt = inv.amount_sats.ok_or(MutinyError::BadAmountError)?;
Expand All @@ -1654,9 +1679,19 @@ impl<S: MutinyStorage> MutinyWallet<S> {

// if invoice amount changed, create a new invoice
let (inv_to_pay, fee) = if first_invoice_amount != inv_amt {
self.node_manager
.create_invoice(first_invoice_amount, labels.clone())
.await?
match to_federation_client {
Some(f) => {
// swap from one federation to another
let inv = f.get_invoice(amt, labels.clone()).await?;
(inv, 0)
}
None => {
// use the lightning node if no to federation selected
self.node_manager
.create_invoice(amt, labels.clone())
.await?
}
}
} else {
(inv.clone(), fee)
};
Expand All @@ -1665,9 +1700,9 @@ impl<S: MutinyStorage> MutinyWallet<S> {
let bolt_11 = inv_to_pay.bolt11.expect("create inv had one job");
self.storage
.set_invoice_labels(bolt_11.clone(), labels.clone())?;
let first_invoice_res = fedimint_client.pay_invoice(bolt_11, labels).await?;
let first_invoice_res = from_fedimint_client.pay_invoice(bolt_11, labels).await?;

let remaining_balance = fedimint_client.get_balance().await?;
let remaining_balance = from_fedimint_client.get_balance().await?;
if remaining_balance > 0 {
// there was a remainder when there shouldn't have been
// for now just log this, it is probably just a millisat/1 sat difference
Expand All @@ -1689,31 +1724,38 @@ impl<S: MutinyStorage> MutinyWallet<S> {
pub async fn estimate_sweep_federation_fee(
&self,
amount: Option<u64>,
from_federation_id: Option<FederationId>,
to_federation_id: Option<FederationId>,
) -> 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 from_federation_id = from_federation_id.unwrap_or(federation_ids[0]);
let federation_lock = self.federations.read().await;
let fedimint_client = federation_lock
.get(federation_id)
.get(&from_federation_id)
.ok_or(MutinyError::NotFound)?;
let fees = fedimint_client.gateway_fee().await?;

let (lsp_fee, federation_fee) = {
if let Some(amt) = amount {
// if the user provided amount, this is easy
(
self.node_manager.get_lsp_fee(amt).await?,
(calc_routing_fee_msat(amt as f64 * 1_000.0, &fees) / 1_000.0).floor() as u64,
)
let incoming_fee = if to_federation_id.is_some() {
0
} else {
self.node_manager.get_lsp_fee(amt).await?
};

let outgoing_fee =
(calc_routing_fee_msat(amt as f64 * 1_000.0, &fees) / 1_000.0).floor() as u64;

(incoming_fee, outgoing_fee)
} else {
// If no amount, figure out the amount to send over
let current_balance = fedimint_client.get_balance().await?;
Expand All @@ -1727,11 +1769,15 @@ impl<S: MutinyStorage> MutinyWallet<S> {
.map_or(Err(MutinyError::InsufficientBalance), Ok)?;
log_debug!(self.logger, "max spendable: {}", amt);

// try to get an invoice for this exact amount
(
self.node_manager.get_lsp_fee(amt).await?,
current_balance - amt,
)
let incoming_fee = if to_federation_id.is_some() {
0
} else {
self.node_manager.get_lsp_fee(amt).await?
};

let outgoing_fee = current_balance - amt;

(incoming_fee, outgoing_fee)
}
};

Expand Down
43 changes: 41 additions & 2 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,16 +1059,55 @@ impl MutinyWallet {
pub async fn sweep_federation_balance(
&self,
amount: Option<u64>,
from_federation_id: Option<String>,
to_federation_id: Option<String>,
) -> Result<FedimintSweepResult, MutinyJsError> {
Ok(self.inner.sweep_federation_balance(amount).await?.into())
let from_federation_id = match from_federation_id {
Some(f) => {
Some(FederationId::from_str(&f).map_err(|_| MutinyJsError::InvalidArgumentsError)?)
}
None => None,
};

let to_federation_id = match to_federation_id {
Some(f) => {
Some(FederationId::from_str(&f).map_err(|_| MutinyJsError::InvalidArgumentsError)?)
}
None => None,
};

Ok(self
.inner
.sweep_federation_balance(amount, from_federation_id, to_federation_id)
.await?
.into())
}

/// Estimate the fee before trying to sweep from federation
pub async fn estimate_sweep_federation_fee(
&self,
amount: Option<u64>,
from_federation_id: Option<String>,
to_federation_id: Option<String>,
) -> Result<Option<u64>, MutinyJsError> {
Ok(self.inner.estimate_sweep_federation_fee(amount).await?)
let from_federation_id = match from_federation_id {
Some(f) => {
Some(FederationId::from_str(&f).map_err(|_| MutinyJsError::InvalidArgumentsError)?)
}
None => None,
};

let to_federation_id = match to_federation_id {
Some(f) => {
Some(FederationId::from_str(&f).map_err(|_| MutinyJsError::InvalidArgumentsError)?)
}
None => None,
};

Ok(self
.inner
.estimate_sweep_federation_fee(amount, from_federation_id, to_federation_id)
.await?)
}

/// Closes a channel with the given outpoint.
Expand Down
Loading