Skip to content

Commit

Permalink
Create or pay invoice from federation
Browse files Browse the repository at this point in the history
  • Loading branch information
TonyGiorgio committed Nov 29, 2023
1 parent 2c5eeb3 commit 1842a44
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 11 deletions.
147 changes: 143 additions & 4 deletions mutiny-core/src/fedimint.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
use crate::{error::MutinyError, logging::MutinyLogger, storage::MutinyStorage};
use crate::{
error::MutinyError, logging::MutinyLogger, nodemanager::MutinyInvoice, storage::MutinyStorage,
HTLCStatus,
};
use bitcoin::{secp256k1::Secp256k1, util::bip32::ExtendedPrivKey};
use bitcoin::{
util::bip32::{ChildNumber, DerivationPath},
Network,
};
use fedimint_client::{derivable_secret::DerivableSecret, ClientArc, FederationInfo};
use fedimint_core::db::mem_impl::MemDatabase;
use fedimint_core::{api::InviteCode, config::FederationId};
use fedimint_ln_client::LightningClientInit;
use fedimint_core::{db::mem_impl::MemDatabase, Amount};
use fedimint_ln_client::{
InternalPayState, LightningClientInit, LightningClientModule, LnPayState, LnReceiveState,
};
use fedimint_mint_client::MintClientInit;
use fedimint_wallet_client::WalletClientInit;
use lightning::log_info;
use futures_util::StreamExt;
use lightning::util::logger::Logger;
use lightning::{log_debug, log_info};
use lightning_invoice::Bolt11Invoice;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
Expand All @@ -20,6 +27,47 @@ use std::{

const FEDIMINT_CLIENT_NONCE: &[u8] = b"Fedimint Client Salt";

impl From<LnReceiveState> for HTLCStatus {
fn from(state: LnReceiveState) -> Self {
match state {
LnReceiveState::Created => HTLCStatus::Pending,
LnReceiveState::Claimed => HTLCStatus::Succeeded,
LnReceiveState::WaitingForPayment { .. } => HTLCStatus::Pending,
LnReceiveState::Canceled { .. } => HTLCStatus::Failed,
LnReceiveState::Funded => HTLCStatus::InFlight,
LnReceiveState::AwaitingFunds => HTLCStatus::InFlight,
}
}
}

impl From<InternalPayState> for HTLCStatus {
fn from(state: InternalPayState) -> Self {
match state {
InternalPayState::Funding => HTLCStatus::InFlight,
InternalPayState::Preimage(_) => HTLCStatus::Succeeded,
InternalPayState::RefundSuccess { .. } => HTLCStatus::Failed,
InternalPayState::RefundError { .. } => HTLCStatus::Failed,
InternalPayState::FundingFailed { .. } => HTLCStatus::Failed,
InternalPayState::UnexpectedError(_) => HTLCStatus::Failed,
}
}
}

impl From<LnPayState> for HTLCStatus {
fn from(state: LnPayState) -> Self {
match state {
LnPayState::Created => HTLCStatus::Pending,
LnPayState::Canceled => HTLCStatus::Failed,
LnPayState::Funded => HTLCStatus::InFlight,
LnPayState::WaitingForRefund { .. } => HTLCStatus::InFlight,
LnPayState::AwaitingChange => HTLCStatus::InFlight,
LnPayState::Success { .. } => HTLCStatus::Succeeded,
LnPayState::Refunded { .. } => HTLCStatus::Failed,
LnPayState::UnexpectedError { .. } => HTLCStatus::Failed,
}
}
}

// This is the FedimintStorage object saved to the DB
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct FedimintStorage {
Expand Down Expand Up @@ -114,6 +162,97 @@ impl<S: MutinyStorage> FedimintClient<S> {
archived: self.fedimint_index.archived,
}
}

pub(crate) async fn get_invoice(&self, amount: u64) -> Result<MutinyInvoice, MutinyError> {
let lightning_module = self
.fedimint_client
.get_first_module::<LightningClientModule>();
// TODO, do we need to do something with ID?
let (_id, invoice) = lightning_module
.create_bolt11_invoice(Amount::from_sats(amount), String::new(), None, ())
.await?;
Ok(invoice.into())
}

/// Get the balance of this fedimint client in sats
pub(crate) async fn get_balance(&self) -> Result<u64, MutinyError> {
Ok(self.fedimint_client.get_balance().await.msats / 1_000)
}

pub(crate) async fn pay_invoice(
&self,
invoice: &Bolt11Invoice,
) -> Result<MutinyInvoice, MutinyError> {
let lightning_module = self
.fedimint_client
.get_first_module::<LightningClientModule>();
let outgoing_payment = lightning_module
.pay_bolt11_invoice(invoice.clone(), ())
.await?;

let mut inv: MutinyInvoice = invoice.clone().into();
match outgoing_payment.payment_type {
fedimint_ln_client::PayType::Internal(pay_id) => {
// TODO merge the two as much as we can
let pay_outcome = lightning_module
.subscribe_internal_pay(pay_id)
.await
.map_err(|_| MutinyError::ConnectionFailed)?; // TODO resume if we error here?

match pay_outcome {
fedimint_client::oplog::UpdateStreamOrOutcome::UpdateStream(mut s) => {
log_debug!(
self.logger,
"waiting for update stream on payment: {}",
pay_id
);
while let Some(outcome) = s.next().await {
log_info!(self.logger, "Outcome: {outcome:?}");
inv.status = outcome.into();

if matches!(inv.status, HTLCStatus::Failed | HTLCStatus::Succeeded) {
break;
}
}
}
fedimint_client::oplog::UpdateStreamOrOutcome::Outcome(o) => {
log_info!(self.logger, "Outcome: {o:?}");
inv.status = o.into();
}
}
}
fedimint_ln_client::PayType::Lightning(pay_id) => {
let pay_outcome = lightning_module
.subscribe_ln_pay(pay_id)
.await
.map_err(|_| MutinyError::ConnectionFailed)?; // TODO resume if we error here?

match pay_outcome {
fedimint_client::oplog::UpdateStreamOrOutcome::UpdateStream(mut s) => {
log_debug!(
self.logger,
"waiting for update stream on payment: {}",
pay_id
);
while let Some(outcome) = s.next().await {
log_info!(self.logger, "Outcome: {outcome:?}");
inv.status = outcome.into();

if matches!(inv.status, HTLCStatus::Failed | HTLCStatus::Succeeded) {
break;
}
}
}
fedimint_client::oplog::UpdateStreamOrOutcome::Outcome(o) => {
log_info!(self.logger, "Outcome: {o:?}");
inv.status = o.into();
}
}
}
}

Ok(inv)
}
}

// A fedimint private key will be derived from `m/1'/X'`, where X is the index of a specific fedimint.
Expand Down
55 changes: 48 additions & 7 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,9 @@ impl<S: MutinyStorage> MutinyWallet<S> {
});
}

/// Pays a lightning invoice from the selected node.
/// Pays a lightning invoice from a federation (preferred) or node.
/// An amount should only be provided if the invoice does not have an amount.
/// Amountless invoices cannot be paid by a federation.
/// The amount should be in satoshis.
pub async fn pay_invoice(
&self,
Expand All @@ -431,6 +432,27 @@ impl<S: MutinyStorage> MutinyWallet<S> {
return Err(MutinyError::IncorrectNetwork(inv.network()));
}

// Prefer the federation spends if possible
let federation_ids = self.list_federations().await?;
if !federation_ids.is_empty() {
// Use the first federation for simplicity
let federation_id = &federation_ids[0];
if let Some(fedimint_client) = self.fedimints.lock().await.get(federation_id) {
// Check the amount specified in the invoice
if let Some(invoice_amount) = inv.amount_milli_satoshis() {
// Check if the federation has enough balance
let balance = fedimint_client.get_balance().await?;
if balance >= invoice_amount / 1_000 {
// Try to pay the invoice using the federation
return fedimint_client.pay_invoice(inv).await;
}
}
// If invoice amount is None or balance is not sufficient, fall through to node_manager payment
}
// If federation client is not found, fall through to node_manager payment
}

// Default to using node_manager for payment
Ok(self
.node_manager
.pay_invoice(None, inv, amt_sats, labels)
Expand Down Expand Up @@ -468,15 +490,34 @@ impl<S: MutinyStorage> MutinyWallet<S> {
amount: Option<u64>,
labels: Vec<String>,
) -> Result<MutinyBip21RawMaterials, MutinyError> {
// If we are in safe mode, we don't create invoices
let invoice = if self.config.safe_mode {
None
} else {
let inv = self
.node_manager
.create_invoice(amount, labels.clone())
.await?;
Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?)
// Check if a federation exists
let federation_ids = self.list_federations().await?;
if !federation_ids.is_empty() {
// Use the first federation for simplicity
let federation_id = &federation_ids[0];
let fedimint_client = self.fedimints.lock().await.get(federation_id).cloned();

match fedimint_client {
Some(client) => {
// Try to create an invoice using the federation
match client.get_invoice(amount.unwrap_or_default()).await {
Ok(inv) => Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?),
Err(_) => None, // Handle the error or fallback to node_manager invoice creation
}
}
None => None, // No federation client found, fallback to node_manager invoice creation
}
} else {
// Fallback to node_manager invoice creation if no federation is found
let inv = self
.node_manager
.create_invoice(amount, labels.clone())
.await?;
Some(inv.bolt11.ok_or(MutinyError::WalletOperationFailed)?)
}
};

let Ok(address) = self.node_manager.get_new_address(labels.clone()) else {
Expand Down

0 comments on commit 1842a44

Please sign in to comment.