feat: lnd ln backend

This commit is contained in:
thesimplekid
2024-08-30 19:27:18 +01:00
parent 847eab8e07
commit 029f922326
11 changed files with 367 additions and 3 deletions

272
crates/cdk-lnd/src/lib.rs Normal file
View File

@@ -0,0 +1,272 @@
//! CDK lightning backend for LND
// Copyright (c) 2023 Steffen (MIT)
#![warn(missing_docs)]
#![warn(rustdoc::bare_urls)]
use std::path::PathBuf;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
use anyhow::anyhow;
use async_trait::async_trait;
use cdk::amount::Amount;
use cdk::cdk_lightning::{
self, to_unit, CreateInvoiceResponse, MintLightning, MintMeltSettings, PayInvoiceResponse,
PaymentQuoteResponse, Settings, MSAT_IN_SAT,
};
use cdk::mint::FeeReserve;
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
use cdk::util::{hex, unix_time};
use cdk::{mint, Bolt11Invoice};
use error::Error;
use fedimint_tonic_lnd::lnrpc::fee_limit::Limit;
use fedimint_tonic_lnd::lnrpc::FeeLimit;
use fedimint_tonic_lnd::Client;
use futures::{Stream, StreamExt};
use tokio::sync::Mutex;
pub mod error;
/// Lnd mint backend
#[derive(Clone)]
pub struct Lnd {
address: String,
cert_file: PathBuf,
macaroon_file: PathBuf,
client: Arc<Mutex<Client>>,
fee_reserve: FeeReserve,
mint_settings: MintMeltSettings,
melt_settings: MintMeltSettings,
}
impl Lnd {
/// Create new [`Lnd`]
pub async fn new(
address: String,
cert_file: PathBuf,
macaroon_file: PathBuf,
fee_reserve: FeeReserve,
mint_settings: MintMeltSettings,
melt_settings: MintMeltSettings,
) -> Result<Self, Error> {
let client = fedimint_tonic_lnd::connect(address.to_string(), &cert_file, &macaroon_file)
.await
.map_err(|err| {
tracing::error!("Connection error: {}", err.to_string());
Error::Connection
})?;
Ok(Self {
address,
cert_file,
macaroon_file,
client: Arc::new(Mutex::new(client)),
fee_reserve,
mint_settings,
melt_settings,
})
}
}
#[async_trait]
impl MintLightning for Lnd {
type Err = cdk_lightning::Error;
fn get_settings(&self) -> Settings {
Settings {
mpp: true,
unit: CurrencyUnit::Msat,
mint_settings: self.mint_settings,
melt_settings: self.melt_settings,
}
}
async fn wait_any_invoice(
&self,
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
let mut client =
fedimint_tonic_lnd::connect(self.address.clone(), &self.cert_file, &self.macaroon_file)
.await
.map_err(|_| Error::Connection)?;
let stream_req = fedimint_tonic_lnd::lnrpc::InvoiceSubscription {
add_index: 0,
settle_index: 0,
};
let stream = client
.lightning()
.subscribe_invoices(stream_req)
.await
.unwrap()
.into_inner();
Ok(futures::stream::unfold(stream, |mut stream| async move {
match stream.message().await {
Ok(Some(msg)) => {
if msg.state == 1 {
Some((hex::encode(msg.r_hash), stream))
} else {
None
}
}
Ok(None) => None, // End of stream
Err(_) => None, // Handle errors gracefully, ends the stream on error
}
})
.boxed())
}
async fn get_payment_quote(
&self,
melt_quote_request: &MeltQuoteBolt11Request,
) -> Result<PaymentQuoteResponse, Self::Err> {
let invoice_amount_msat = melt_quote_request
.request
.amount_milli_satoshis()
.ok_or(Error::UnknownInvoiceAmount)?;
let amount = to_unit(
invoice_amount_msat,
&CurrencyUnit::Msat,
&melt_quote_request.unit,
)?;
let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
let fee = match relative_fee_reserve > absolute_fee_reserve {
true => relative_fee_reserve,
false => absolute_fee_reserve,
};
Ok(PaymentQuoteResponse {
request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
amount,
fee: fee.into(),
state: MeltQuoteState::Unpaid,
})
}
async fn pay_invoice(
&self,
melt_quote: mint::MeltQuote,
partial_amount: Option<Amount>,
max_fee: Option<Amount>,
) -> Result<PayInvoiceResponse, Self::Err> {
let payment_request = melt_quote.request;
let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
payment_request,
fee_limit: max_fee.map(|f| {
let limit = Limit::Fixed(u64::from(f) as i64);
FeeLimit { limit: Some(limit) }
}),
amt_msat: partial_amount
.map(|a| {
let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat).unwrap();
u64::from(msat) as i64
})
.unwrap_or_default(),
..Default::default()
};
let payment_response = self
.client
.lock()
.await
.lightning()
.send_payment_sync(fedimint_tonic_lnd::tonic::Request::new(pay_req))
.await
.unwrap()
.into_inner();
let total_spent = payment_response
.payment_route
.map_or(0, |route| route.total_fees_msat / MSAT_IN_SAT as i64)
as u64;
Ok(PayInvoiceResponse {
payment_hash: hex::encode(payment_response.payment_hash),
payment_preimage: Some(hex::encode(payment_response.payment_preimage)),
status: MeltQuoteState::Pending,
total_spent: total_spent.into(),
})
}
async fn create_invoice(
&self,
amount: Amount,
unit: &CurrencyUnit,
description: String,
unix_expiry: u64,
) -> Result<CreateInvoiceResponse, Self::Err> {
let time_now = unix_time();
assert!(unix_expiry > time_now);
let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
let invoice_request = fedimint_tonic_lnd::lnrpc::Invoice {
value_msat: u64::from(amount) as i64,
memo: description,
..Default::default()
};
let invoice = self
.client
.lock()
.await
.lightning()
.add_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request))
.await
.unwrap()
.into_inner();
let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
Ok(CreateInvoiceResponse {
request_lookup_id: bolt11.payment_hash().to_string(),
request: bolt11,
expiry: Some(unix_expiry),
})
}
async fn check_invoice_status(
&self,
request_lookup_id: &str,
) -> Result<MintQuoteState, Self::Err> {
let invoice_request = fedimint_tonic_lnd::lnrpc::PaymentHash {
r_hash: hex::decode(request_lookup_id).unwrap(),
..Default::default()
};
let invoice = self
.client
.lock()
.await
.lightning()
.lookup_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request))
.await
.unwrap()
.into_inner();
match invoice.state {
// Open
0 => Ok(MintQuoteState::Unpaid),
// Settled
1 => Ok(MintQuoteState::Paid),
// Canceled
2 => Ok(MintQuoteState::Unpaid),
// Accepted
3 => Ok(MintQuoteState::Unpaid),
_ => Err(Self::Err::Anyhow(anyhow!("Invalid status"))),
}
}
}