浏览代码

feat: lnd ln backend

thesimplekid 6 月之前
父节点
当前提交
029f922326

+ 3 - 0
.github/workflows/ci.yml

@@ -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

+ 1 - 0
Cargo.toml

@@ -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 - 0
crates/cdk-lnd/Cargo.toml

@@ -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 - 0
crates/cdk-lnd/src/error.rs

@@ -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 - 0
crates/cdk-lnd/src/lib.rs

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

+ 1 - 0
crates/cdk-mintd/Cargo.toml

@@ -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 }

+ 6 - 1
crates/cdk-mintd/example.config.toml

@@ -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 = ""

+ 10 - 0
crates/cdk-mintd/src/config.rs

@@ -24,6 +24,7 @@ pub enum LnBackend {
     LNbits,
     FakeWallet,
     Phoenixd,
+    Lnd,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -53,6 +54,13 @@ pub struct Cln {
 }
 
 #[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,
     pub api_url: 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 => (),
         }
 

+ 29 - 0
crates/cdk-mintd/src/main.rs

@@ -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;
 

+ 1 - 1
flake.nix

@@ -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 - 1
rustfmt.toml

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