mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-22 13:36:00 +01:00
feat: lnd ln backend
This commit is contained in:
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
20
crates/cdk-lnd/Cargo.toml
Normal 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
|
||||
23
crates/cdk-lnd/src/error.rs
Normal file
23
crates/cdk-lnd/src/error.rs
Normal 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
272
crates/cdk-lnd/src/lib.rs
Normal 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"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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 => (),
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 ];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
tab_spaces = 4
|
||||
max_width = 80
|
||||
max_width = 100
|
||||
newline_style = "Auto"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
|
||||
Reference in New Issue
Block a user