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

View File

@@ -34,6 +34,7 @@ jobs:
-p cdk-sqlite,
-p cdk-axum,
-p cdk-cln,
-p cdk-lnd,
-p cdk-phoenixd,
-p cdk-strike,
-p cdk-lnbits
@@ -53,6 +54,8 @@ jobs:
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: Install protobuf
run: sudo apt-get install -y protobuf-compiler
- name: Set default toolchain
run: rustup default ${{ matrix.rust.version }}
- name: Set profile

View File

@@ -38,6 +38,7 @@ cdk-phoenixd = { version = "0.3", path = "./crates/cdk-phoenixd", default-featur
cdk-axum = { version = "0.3", path = "./crates/cdk-axum", default-features = false }
cdk-fake-wallet = { version = "0.3", path = "./crates/cdk-fake-wallet", default-features = false }
cdk-strike = { version = "0.3", path = "./crates/cdk-strike", default-features = false }
cdk-lnd = { version = "0.3", path = "./crates/cdk-lnd", default-features = false }
tokio = { version = "1", default-features = false }
thiserror = "1"
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }

20
crates/cdk-lnd/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[package]
name = "cdk-lnd"
version = { workspace = true }
edition = "2021"
authors = ["CDK Developers"]
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true # MSRV
license.workspace = true
description = "CDK ln backend for lnd"
[dependencies]
async-trait.workspace = true
anyhow.workspace = true
cdk = { workspace = true, default-features = false, features = ["mint"] }
fedimint-tonic-lnd = "0.2.0"
futures.workspace = true
tokio.workspace = true
tracing.workspace = true
thiserror.workspace = true

View File

@@ -0,0 +1,23 @@
//! LND Errors
use thiserror::Error;
/// LND Error
#[derive(Debug, Error)]
pub enum Error {
/// Invoice amount not defined
#[error("Unknown invoice amount")]
UnknownInvoiceAmount,
/// Unknown invoice
#[error("Unknown invoice")]
UnknownInvoice,
/// Connection Error
#[error("LND connection error")]
Connection,
}
impl From<Error> for cdk::cdk_lightning::Error {
fn from(e: Error) -> Self {
Self::Lightning(Box::new(e))
}
}

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"))),
}
}
}

View File

@@ -18,6 +18,7 @@ cdk-sqlite = { workspace = true, default-features = false, features = ["mint"] }
cdk-cln = { workspace = true, default-features = false }
cdk-lnbits = { workspace = true, default-features = false }
cdk-phoenixd = { workspace = true, default-features = false }
cdk-lnd = { workspace = true, default-features = false }
cdk-fake-wallet = { workspace = true, default-features = false }
cdk-strike.workspace = true
cdk-axum = { workspace = true, default-features = false }

View File

@@ -26,7 +26,7 @@ mnemonic = ""
[ln]
# Required ln backend `cln`, `strike`, `fakewallet`
# Required ln backend `cln`, `lnd`, `strike`, `fakewallet`
ln_backend = "cln"
# [cln]
@@ -47,3 +47,8 @@ ln_backend = "cln"
# [phoenixd]
# api_password = ""
# api_url = ""
# [lnd]
# address = ""
# macaroon_file = ""
# cert_file = ""

View File

@@ -24,6 +24,7 @@ pub enum LnBackend {
LNbits,
FakeWallet,
Phoenixd,
Lnd,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -52,6 +53,13 @@ pub struct Cln {
pub rpc_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Lnd {
pub address: String,
pub cert_file: PathBuf,
pub macaroon_file: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Phoenixd {
pub api_password: String,
@@ -93,6 +101,7 @@ pub struct Settings {
pub strike: Option<Strike>,
pub lnbits: Option<LNbits>,
pub phoenixd: Option<Phoenixd>,
pub lnd: Option<Lnd>,
pub fake_wallet: Option<FakeWallet>,
pub database: Database,
}
@@ -159,6 +168,7 @@ impl Settings {
LnBackend::Strike => assert!(settings.strike.is_some()),
LnBackend::LNbits => assert!(settings.lnbits.is_some()),
LnBackend::Phoenixd => assert!(settings.phoenixd.is_some()),
LnBackend::Lnd => assert!(settings.lnd.is_some()),
LnBackend::FakeWallet => (),
}

View File

@@ -24,6 +24,7 @@ use cdk_axum::LnKey;
use cdk_cln::Cln;
use cdk_fake_wallet::FakeWallet;
use cdk_lnbits::LNbits;
use cdk_lnd::Lnd;
use cdk_phoenixd::Phoenixd;
use cdk_redb::MintRedbDatabase;
use cdk_sqlite::MintSqliteDatabase;
@@ -286,6 +287,34 @@ async fn main() -> anyhow::Result<()> {
vec![router]
}
LnBackend::Lnd => {
let lnd_settings = settings.lnd.expect("Checked at config load");
let address = lnd_settings.address;
let cert_file = lnd_settings.cert_file;
let macaroon_file = lnd_settings.macaroon_file;
let lnd = Lnd::new(
address,
cert_file,
macaroon_file,
fee_reserve,
MintMeltSettings::default(),
MintMeltSettings::default(),
)
.await?;
supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64));
ln_backends.insert(
LnKey {
unit: CurrencyUnit::Sat,
method: PaymentMethod::Bolt11,
},
Arc::new(lnd),
);
vec![]
}
LnBackend::FakeWallet => {
let units = settings.fake_wallet.unwrap_or_default().supported_units;

View File

@@ -54,7 +54,7 @@
devShells = flakeboxLib.mkShells {
toolchain = toolchainNative;
packages = [ ];
nativeBuildInputs = with pkgs; [ wasm-pack sqlx-cli ];
nativeBuildInputs = with pkgs; [ wasm-pack sqlx-cli protobuf3_20 ];
};
});
}

View File

@@ -1,5 +1,5 @@
tab_spaces = 4
max_width = 80
max_width = 100
newline_style = "Auto"
reorder_imports = true
reorder_modules = true