thesimplekid 2 місяців тому
батько
коміт
162507c492
57 змінених файлів з 2460 додано та 357 видалено
  1. 28 0
      .github/workflows/ci.yml
  2. 10 0
      Cargo.toml
  3. 6 3
      crates/cashu/src/nuts/nut00/mod.rs
  4. 1 0
      crates/cdk-cln/Cargo.toml
  5. 1 1
      crates/cdk-cln/src/error.rs
  6. 43 32
      crates/cdk-cln/src/lib.rs
  7. 11 2
      crates/cdk-common/src/common.rs
  8. 3 3
      crates/cdk-common/src/database/mint.rs
  9. 2 2
      crates/cdk-common/src/error.rs
  10. 1 1
      crates/cdk-common/src/lib.rs
  11. 55 26
      crates/cdk-common/src/payment.rs
  12. 1 1
      crates/cdk-fake-wallet/Cargo.toml
  13. 1 1
      crates/cdk-fake-wallet/src/error.rs
  14. 55 35
      crates/cdk-fake-wallet/src/lib.rs
  15. 12 10
      crates/cdk-integration-tests/src/init_pure_tests.rs
  16. 1 1
      crates/cdk-integration-tests/src/init_regtest.rs
  17. 10 8
      crates/cdk-integration-tests/tests/mint.rs
  18. 176 0
      crates/cdk-integration-tests/tests/payment_processor.rs
  19. 1 0
      crates/cdk-lnbits/Cargo.toml
  20. 1 1
      crates/cdk-lnbits/src/error.rs
  21. 54 35
      crates/cdk-lnbits/src/lib.rs
  22. 1 0
      crates/cdk-lnd/Cargo.toml
  23. 1 1
      crates/cdk-lnd/src/error.rs
  24. 58 48
      crates/cdk-lnd/src/lib.rs
  25. 3 7
      crates/cdk-mint-rpc/Cargo.toml
  26. 9 7
      crates/cdk-mintd/Cargo.toml
  27. 7 0
      crates/cdk-mintd/example.config.toml
  28. 21 3
      crates/cdk-mintd/src/config.rs
  29. 44 0
      crates/cdk-mintd/src/env_vars/grpc_processor.rs
  30. 2 0
      crates/cdk-mintd/src/env_vars/ln.rs
  31. 9 0
      crates/cdk-mintd/src/env_vars/mod.rs
  32. 102 37
      crates/cdk-mintd/src/main.rs
  33. 29 3
      crates/cdk-mintd/src/setup.rs
  34. 65 0
      crates/cdk-payment-processor/Cargo.toml
  35. 77 0
      crates/cdk-payment-processor/README.md
  36. 5 0
      crates/cdk-payment-processor/build.rs
  37. 206 0
      crates/cdk-payment-processor/src/bin/payment_processor.rs
  38. 20 0
      crates/cdk-payment-processor/src/error.rs
  39. 8 0
      crates/cdk-payment-processor/src/lib.rs
  40. 299 0
      crates/cdk-payment-processor/src/proto/client.rs
  41. 207 0
      crates/cdk-payment-processor/src/proto/mod.rs
  42. 113 0
      crates/cdk-payment-processor/src/proto/payment_processor.proto
  43. 345 0
      crates/cdk-payment-processor/src/proto/server.rs
  44. 3 3
      crates/cdk-redb/src/mint/mod.rs
  45. 2 2
      crates/cdk-sqlite/src/mint/memory.rs
  46. 7 5
      crates/cdk-sqlite/src/mint/mod.rs
  47. 1 1
      crates/cdk/src/lib.rs
  48. 19 13
      crates/cdk/src/mint/builder.rs
  49. 3 3
      crates/cdk/src/mint/ln.rs
  50. 31 21
      crates/cdk/src/mint/melt.rs
  51. 14 7
      crates/cdk/src/mint/mint_nut04.rs
  52. 19 18
      crates/cdk/src/mint/mod.rs
  53. 3 3
      crates/cdk/src/mint/start_up_check.rs
  54. 1 1
      crates/cdk/src/wallet/mod.rs
  55. 99 3
      justfile
  56. 154 0
      misc/mintd_payment_processor.sh
  57. 0 9
      misc/test.just

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

@@ -104,6 +104,7 @@ jobs:
             -p cdk-lnd,
             -p cdk-lnbits,
             -p cdk-fake-wallet,
+            -p cdk-payment-processor,
             --bin cdk-cli,
             --bin cdk-cli --features sqlcipher,
             --bin cdk-mintd,
@@ -115,9 +116,11 @@ jobs:
             --bin cdk-mintd --no-default-features --features cln,
             --bin cdk-mintd --no-default-features --features lnbits,
             --bin cdk-mintd --no-default-features --features fakewallet,
+            --bin cdk-mintd --no-default-features --features grpc-processor,
             --bin cdk-mintd --no-default-features --features "management-rpc lnd",
             --bin cdk-mintd --no-default-features --features "management-rpc cln",
             --bin cdk-mintd --no-default-features --features "management-rpc lnbits",
+            --bin cdk-mintd --no-default-features --features "management-rpc grpc-processor",
             --bin cdk-mintd --no-default-features --features "swagger lnd",
             --bin cdk-mintd --no-default-features --features "swagger cln",
             --bin cdk-mintd --no-default-features --features "swagger lnbits",
@@ -211,6 +214,30 @@ jobs:
       - name: Test fake mint
         run: nix develop -i -L .#stable --command just test
 
+
+  payment-processor-itests:
+    name: "Payment processor tests"
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        ln: 
+          [
+          FAKEWALLET,
+          CLN,
+          LND
+          ]
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v11
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@v6
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+      - name: Test
+        run: nix develop -i -L .#stable --command just itest-payment-processor ${{matrix.ln}}
+
   msrv-build:
     name: "MSRV build"
     runs-on: ubuntu-latest
@@ -231,6 +258,7 @@ jobs:
             -p cdk-mint-rpc,
             -p cdk-sqlite,
             -p cdk-mintd,
+            -p cdk-payment-processor --no-default-features,
           ]
     steps:
       - name: checkout

+ 10 - 0
Cargo.toml

@@ -25,6 +25,7 @@ cdk-cln = { path = "./crates/cdk-cln", version = "=0.7.1" }
 cdk-lnbits = { path = "./crates/cdk-lnbits", version = "=0.7.1" }
 cdk-lnd = { path = "./crates/cdk-lnd", version = "=0.7.1" }
 cdk-fake-wallet = { path = "./crates/cdk-fake-wallet", version = "=0.7.1" }
+cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-features = true, version = "=0.7.1" }
 cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.7.1" }
 cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.7.1" }
 cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.7.1" }
@@ -40,6 +41,7 @@ tokio = { version = "1", default-features = false, features = ["rt", "macros", "
 tokio-util = { version = "0.7.11", default-features = false }
 tower-http = { version = "0.6.1", features = ["compression-full", "decompression-full", "cors", "trace"] }
 tokio-tungstenite = { version = "0.26.0", default-features = false }
+tokio-stream = "0.1.15"
 tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
 tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
 url = "2.3"
@@ -63,6 +65,14 @@ once_cell = "1.20.2"
 instant = { version = "0.1", default-features = false }
 rand = "0.8.5"
 home = "0.5.5"
+tonic = { version = "0.12.3", features = [
+    "channel",
+    "tls",
+    "tls-webpki-roots",
+] }
+prost = "0.13.1"
+tonic-build = "0.12"
+
 
 
 [workspace.metadata]

+ 6 - 3
crates/cashu/src/nuts/nut00/mod.rs

@@ -455,20 +455,22 @@ impl<'de> Deserialize<'de> for CurrencyUnit {
 
 /// Payment Method
 #[non_exhaustive]
-#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub enum PaymentMethod {
     /// Bolt11 payment type
     #[default]
     Bolt11,
+    /// Custom
+    Custom(String),
 }
 
 impl FromStr for PaymentMethod {
     type Err = Error;
     fn from_str(value: &str) -> Result<Self, Self::Err> {
-        match value {
+        match value.to_lowercase().as_str() {
             "bolt11" => Ok(Self::Bolt11),
-            _ => Err(Error::UnsupportedPaymentMethod),
+            c => Ok(Self::Custom(c.to_string())),
         }
     }
 }
@@ -477,6 +479,7 @@ impl fmt::Display for PaymentMethod {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             PaymentMethod::Bolt11 => write!(f, "bolt11"),
+            PaymentMethod::Custom(p) => write!(f, "{}", p),
         }
     }
 }

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

@@ -20,3 +20,4 @@ tokio-util.workspace = true
 tracing.workspace = true
 thiserror.workspace = true
 uuid.workspace = true
+serde_json.workspace = true

+ 1 - 1
crates/cdk-cln/src/error.rs

@@ -28,7 +28,7 @@ pub enum Error {
     Amount(#[from] cdk::amount::Error),
 }
 
-impl From<Error> for cdk::cdk_lightning::Error {
+impl From<Error> for cdk::cdk_payment::Error {
     fn from(e: Error) -> Self {
         Self::Lightning(Box::new(e))
     }

+ 43 - 32
crates/cdk-cln/src/lib.rs

@@ -10,12 +10,13 @@ use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 
 use async_trait::async_trait;
-use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
-use cdk::cdk_lightning::{
-    self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
+use cdk::amount::{to_unit, Amount};
+use cdk::cdk_payment::{
+    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
+    PaymentQuoteResponse,
 };
-use cdk::mint::FeeReserve;
-use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
+use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
+use cdk::types::FeeReserve;
 use cdk::util::{hex, unix_time};
 use cdk::{mint, Bolt11Invoice};
 use cln_rpc::model::requests::{
@@ -28,6 +29,7 @@ use cln_rpc::model::responses::{
 use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny};
 use error::Error;
 use futures::{Stream, StreamExt};
+use serde_json::Value;
 use tokio::sync::Mutex;
 use tokio_util::sync::CancellationToken;
 use uuid::Uuid;
@@ -60,15 +62,15 @@ impl Cln {
 }
 
 #[async_trait]
-impl MintLightning for Cln {
-    type Err = cdk_lightning::Error;
+impl MintPayment for Cln {
+    type Err = cdk_payment::Error;
 
-    fn get_settings(&self) -> Settings {
-        Settings {
+    async fn get_settings(&self) -> Result<Value, Self::Err> {
+        Ok(serde_json::to_value(Bolt11Settings {
             mpp: true,
             unit: CurrencyUnit::Msat,
             invoice_description: true,
-        }
+        })?)
     }
 
     /// Is wait invoice active
@@ -81,7 +83,7 @@ impl MintLightning for Cln {
         self.wait_invoice_cancel_token.cancel()
     }
 
-    async fn wait_any_invoice(
+    async fn wait_any_incoming_payment(
         &self,
     ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
         let last_pay_index = self.get_last_pay_index().await?;
@@ -175,11 +177,21 @@ impl MintLightning for Cln {
 
     async fn get_payment_quote(
         &self,
-        melt_quote_request: &MeltQuoteBolt11Request,
+        request: &str,
+        unit: &CurrencyUnit,
+        options: Option<MeltOptions>,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        let amount = melt_quote_request.amount_msat()?;
+        let bolt11 = Bolt11Invoice::from_str(request)?;
+
+        let amount_msat = match options {
+            Some(amount) => amount.amount_msat(),
+            None => bolt11
+                .amount_milli_satoshis()
+                .ok_or(Error::UnknownInvoiceAmount)?
+                .into(),
+        };
 
-        let amount = amount / MSAT_IN_SAT.into();
+        let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -192,19 +204,19 @@ impl MintLightning for Cln {
         };
 
         Ok(PaymentQuoteResponse {
-            request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
+            request_lookup_id: bolt11.payment_hash().to_string(),
             amount,
             fee: fee.into(),
             state: MeltQuoteState::Unpaid,
         })
     }
 
-    async fn pay_invoice(
+    async fn make_payment(
         &self,
         melt_quote: mint::MeltQuote,
         partial_amount: Option<Amount>,
         max_fee: Option<Amount>,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
         let pay_state = self
             .check_outgoing_payment(&bolt11.payment_hash().to_string())
@@ -271,8 +283,8 @@ impl MintLightning for Cln {
                     PayStatus::FAILED => MeltQuoteState::Failed,
                 };
 
-                PayInvoiceResponse {
-                    payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())),
+                MakePaymentResponse {
+                    payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())),
                     payment_lookup_id: pay_response.payment_hash.to_string(),
                     status,
                     total_spent: to_unit(
@@ -292,15 +304,14 @@ impl MintLightning for Cln {
         Ok(response)
     }
 
-    async fn create_invoice(
+    async fn create_incoming_payment_request(
         &self,
         amount: Amount,
         unit: &CurrencyUnit,
         description: String,
-        unix_expiry: u64,
-    ) -> Result<CreateInvoiceResponse, Self::Err> {
+        unix_expiry: Option<u64>,
+    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         let time_now = unix_time();
-        assert!(unix_expiry > time_now);
 
         let mut cln_client = self.cln_client.lock().await;
 
@@ -314,7 +325,7 @@ impl MintLightning for Cln {
                 amount_msat,
                 description,
                 label: label.clone(),
-                expiry: Some(unix_expiry - time_now),
+                expiry: unix_expiry.map(|t| t - time_now),
                 fallbacks: None,
                 preimage: None,
                 cltv: None,
@@ -328,14 +339,14 @@ impl MintLightning for Cln {
         let expiry = request.expires_at().map(|t| t.as_secs());
         let payment_hash = request.payment_hash();
 
-        Ok(CreateInvoiceResponse {
+        Ok(CreateIncomingPaymentResponse {
             request_lookup_id: payment_hash.to_string(),
-            request,
+            request: request.to_string(),
             expiry,
         })
     }
 
-    async fn check_incoming_invoice_status(
+    async fn check_incoming_payment_status(
         &self,
         payment_hash: &str,
     ) -> Result<MintQuoteState, Self::Err> {
@@ -371,7 +382,7 @@ impl MintLightning for Cln {
     async fn check_outgoing_payment(
         &self,
         payment_hash: &str,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let mut cln_client = self.cln_client.lock().await;
 
         let listpays_response = cln_client
@@ -390,9 +401,9 @@ impl MintLightning for Cln {
             Some(pays_response) => {
                 let status = cln_pays_status_to_mint_state(pays_response.status);
 
-                Ok(PayInvoiceResponse {
+                Ok(MakePaymentResponse {
                     payment_lookup_id: pays_response.payment_hash.to_string(),
-                    payment_preimage: pays_response.preimage.map(|p| hex::encode(p.to_vec())),
+                    payment_proof: pays_response.preimage.map(|p| hex::encode(p.to_vec())),
                     status,
                     total_spent: pays_response
                         .amount_sent_msat
@@ -400,9 +411,9 @@ impl MintLightning for Cln {
                     unit: CurrencyUnit::Msat,
                 })
             }
-            None => Ok(PayInvoiceResponse {
+            None => Ok(MakePaymentResponse {
                 payment_lookup_id: payment_hash.to_string(),
-                payment_preimage: None,
+                payment_proof: None,
                 status: MeltQuoteState::Unknown,
                 total_spent: Amount::ZERO,
                 unit: CurrencyUnit::Msat,

+ 11 - 2
crates/cdk-common/src/common.rs

@@ -143,14 +143,14 @@ impl ProofInfo {
 /// Key used in hashmap of ln backends to identify what unit and payment method
 /// it is for
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct LnKey {
+pub struct PaymentProcessorKey {
     /// Unit of Payment backend
     pub unit: CurrencyUnit,
     /// Method of payment backend
     pub method: PaymentMethod,
 }
 
-impl LnKey {
+impl PaymentProcessorKey {
     /// Create new [`LnKey`]
     pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
         Self { unit, method }
@@ -241,3 +241,12 @@ mod tests {
         assert_eq!(melted.total_amount(), Amount::from(32));
     }
 }
+
+/// Mint Fee Reserve
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct FeeReserve {
+    /// Absolute expected min fee
+    pub min_fee_reserve: Amount,
+    /// Percentage expected fee
+    pub percent_fee_reserve: f32,
+}

+ 3 - 3
crates/cdk-common/src/database/mint.rs

@@ -7,7 +7,7 @@ use cashu::MintInfo;
 use uuid::Uuid;
 
 use super::Error;
-use crate::common::{LnKey, QuoteTTL};
+use crate::common::{PaymentProcessorKey, QuoteTTL};
 use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote};
 use crate::nuts::{
     BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof,
@@ -76,13 +76,13 @@ pub trait Database {
     async fn add_melt_request(
         &self,
         melt_request: MeltBolt11Request<Uuid>,
-        ln_key: LnKey,
+        ln_key: PaymentProcessorKey,
     ) -> Result<(), Self::Err>;
     /// Get melt request
     async fn get_melt_request(
         &self,
         quote_id: &Uuid,
-    ) -> Result<Option<(MeltBolt11Request<Uuid>, LnKey)>, Self::Err>;
+    ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err>;
 
     /// Add [`MintKeySetInfo`]
     async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>;

+ 2 - 2
crates/cdk-common/src/error.rs

@@ -264,10 +264,10 @@ pub enum Error {
     /// Database Error
     #[error(transparent)]
     Database(#[from] crate::database::Error),
-    /// Lightning Error
+    /// Payment Error
     #[error(transparent)]
     #[cfg(feature = "mint")]
-    Lightning(#[from] crate::lightning::Error),
+    Payment(#[from] crate::payment::Error),
 }
 
 /// CDK Error Response

+ 1 - 1
crates/cdk-common/src/lib.rs

@@ -10,7 +10,7 @@ pub mod common;
 pub mod database;
 pub mod error;
 #[cfg(feature = "mint")]
-pub mod lightning;
+pub mod payment;
 pub mod pub_sub;
 pub mod subscription;
 pub mod ws;

+ 55 - 26
crates/cdk-common/src/lightning.rs → crates/cdk-common/src/payment.rs

@@ -3,12 +3,14 @@
 use std::pin::Pin;
 
 use async_trait::async_trait;
+use cashu::MeltOptions;
 use futures::Stream;
-use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError};
+use lightning_invoice::ParseOrSemanticError;
 use serde::{Deserialize, Serialize};
+use serde_json::Value;
 use thiserror::Error;
 
-use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
+use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
 use crate::{mint, Amount};
 
 /// CDK Lightning Error
@@ -23,6 +25,9 @@ pub enum Error {
     /// Unsupported unit
     #[error("Unsupported unit")]
     UnsupportedUnit,
+    /// Unsupported payment option
+    #[error("Unsupported payment option")]
+    UnsupportedPaymentOption,
     /// Payment state is unknown
     #[error("Payment state is unknown")]
     UnknownPaymentState,
@@ -41,47 +46,55 @@ pub enum Error {
     /// Amount Error
     #[error(transparent)]
     Amount(#[from] crate::amount::Error),
+    /// NUT04 Error
+    #[error(transparent)]
+    NUT04(#[from] crate::nuts::nut04::Error),
     /// NUT05 Error
     #[error(transparent)]
     NUT05(#[from] crate::nuts::nut05::Error),
+    /// Custom
+    #[error("`{0}`")]
+    Custom(String),
 }
 
-/// MintLighting Trait
+/// Mint payment trait
 #[async_trait]
-pub trait MintLightning {
+pub trait MintPayment {
     /// Mint Lightning Error
     type Err: Into<Error> + From<Error>;
 
-    /// Base Unit
-    fn get_settings(&self) -> Settings;
+    /// Base Settings
+    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err>;
 
     /// Create a new invoice
-    async fn create_invoice(
+    async fn create_incoming_payment_request(
         &self,
         amount: Amount,
         unit: &CurrencyUnit,
         description: String,
-        unix_expiry: u64,
-    ) -> Result<CreateInvoiceResponse, Self::Err>;
+        unix_expiry: Option<u64>,
+    ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
 
     /// Get payment quote
     /// Used to get fee and amount required for a payment request
     async fn get_payment_quote(
         &self,
-        melt_quote_request: &MeltQuoteBolt11Request,
+        request: &str,
+        unit: &CurrencyUnit,
+        options: Option<MeltOptions>,
     ) -> Result<PaymentQuoteResponse, Self::Err>;
 
-    /// Pay bolt11 invoice
-    async fn pay_invoice(
+    /// Pay request
+    async fn make_payment(
         &self,
         melt_quote: mint::MeltQuote,
         partial_amount: Option<Amount>,
         max_fee_amount: Option<Amount>,
-    ) -> Result<PayInvoiceResponse, Self::Err>;
+    ) -> Result<MakePaymentResponse, Self::Err>;
 
     /// Listen for invoices to be paid to the mint
     /// Returns a stream of request_lookup_id once invoices are paid
-    async fn wait_any_invoice(
+    async fn wait_any_incoming_payment(
         &self,
     ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err>;
 
@@ -92,7 +105,7 @@ pub trait MintLightning {
     fn cancel_wait_invoice(&self);
 
     /// Check the status of an incoming payment
-    async fn check_incoming_invoice_status(
+    async fn check_incoming_payment_status(
         &self,
         request_lookup_id: &str,
     ) -> Result<MintQuoteState, Self::Err>;
@@ -101,27 +114,27 @@ pub trait MintLightning {
     async fn check_outgoing_payment(
         &self,
         request_lookup_id: &str,
-    ) -> Result<PayInvoiceResponse, Self::Err>;
+    ) -> Result<MakePaymentResponse, Self::Err>;
 }
 
-/// Create invoice response
+/// Create incoming payment response
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct CreateInvoiceResponse {
-    /// Id that is used to look up the invoice from the ln backend
+pub struct CreateIncomingPaymentResponse {
+    /// Id that is used to look up the payment from the ln backend
     pub request_lookup_id: String,
-    /// Bolt11 payment request
-    pub request: Bolt11Invoice,
+    /// Payment request
+    pub request: String,
     /// Unix Expiry of Invoice
     pub expiry: Option<u64>,
 }
 
-/// Pay invoice response
+/// Payment response
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct PayInvoiceResponse {
+pub struct MakePaymentResponse {
     /// Payment hash
     pub payment_lookup_id: String,
-    /// Payment Preimage
-    pub payment_preimage: Option<String>,
+    /// Payment proof
+    pub payment_proof: Option<String>,
     /// Status
     pub status: MeltQuoteState,
     /// Total Amount Spent
@@ -145,7 +158,7 @@ pub struct PaymentQuoteResponse {
 
 /// Ln backend settings
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct Settings {
+pub struct Bolt11Settings {
     /// MPP supported
     pub mpp: bool,
     /// Base unit of backend
@@ -153,3 +166,19 @@ pub struct Settings {
     /// Invoice Description supported
     pub invoice_description: bool,
 }
+
+impl TryFrom<Bolt11Settings> for Value {
+    type Error = crate::error::Error;
+
+    fn try_from(value: Bolt11Settings) -> Result<Self, Self::Error> {
+        serde_json::to_value(value).map_err(|err| err.into())
+    }
+}
+
+impl TryFrom<Value> for Bolt11Settings {
+    type Error = crate::error::Error;
+
+    fn try_from(value: Value) -> Result<Self, Self::Error> {
+        serde_json::from_value(value).map_err(|err| err.into())
+    }
+}

+ 1 - 1
crates/cdk-fake-wallet/Cargo.toml

@@ -21,4 +21,4 @@ thiserror.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 lightning-invoice.workspace = true
-tokio-stream = "0.1.15"
+tokio-stream.workspace = true

+ 1 - 1
crates/cdk-fake-wallet/src/error.rs

@@ -16,7 +16,7 @@ pub enum Error {
     NoReceiver,
 }
 
-impl From<Error> for cdk::cdk_lightning::Error {
+impl From<Error> for cdk::cdk_payment::Error {
     fn from(e: Error) -> Self {
         Self::Lightning(Box::new(e))
     }

+ 55 - 35
crates/cdk-fake-wallet/src/lib.rs

@@ -15,23 +15,25 @@ use async_trait::async_trait;
 use bitcoin::hashes::{sha256, Hash};
 use bitcoin::secp256k1::rand::{thread_rng, Rng};
 use bitcoin::secp256k1::{Secp256k1, SecretKey};
-use cdk::amount::{Amount, MSAT_IN_SAT};
-use cdk::cdk_lightning::{
-    self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
+use cdk::amount::{to_unit, Amount};
+use cdk::cdk_payment::{
+    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
+    PaymentQuoteResponse,
 };
-use cdk::mint::FeeReserve;
-use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
-use cdk::util::unix_time;
+use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
+use cdk::types::FeeReserve;
 use cdk::{ensure_cdk, mint};
 use error::Error;
 use futures::stream::StreamExt;
 use futures::Stream;
 use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
 use serde::{Deserialize, Serialize};
+use serde_json::Value;
 use tokio::sync::Mutex;
 use tokio::time;
 use tokio_stream::wrappers::ReceiverStream;
 use tokio_util::sync::CancellationToken;
+use tracing::instrument;
 
 pub mod error;
 
@@ -49,7 +51,7 @@ pub struct FakeWallet {
 }
 
 impl FakeWallet {
-    /// Creat new [`FakeWallet`]
+    /// Create new [`FakeWallet`]
     pub fn new(
         fee_reserve: FeeReserve,
         payment_states: HashMap<String, MeltQuoteState>,
@@ -96,40 +98,56 @@ impl Default for FakeInvoiceDescription {
 }
 
 #[async_trait]
-impl MintLightning for FakeWallet {
-    type Err = cdk_lightning::Error;
+impl MintPayment for FakeWallet {
+    type Err = cdk_payment::Error;
 
-    fn get_settings(&self) -> Settings {
-        Settings {
+    #[instrument(skip_all)]
+    async fn get_settings(&self) -> Result<Value, Self::Err> {
+        Ok(serde_json::to_value(Bolt11Settings {
             mpp: true,
             unit: CurrencyUnit::Msat,
             invoice_description: true,
-        }
+        })?)
     }
 
+    #[instrument(skip_all)]
     fn is_wait_invoice_active(&self) -> bool {
         self.wait_invoice_is_active.load(Ordering::SeqCst)
     }
 
+    #[instrument(skip_all)]
     fn cancel_wait_invoice(&self) {
         self.wait_invoice_cancel_token.cancel()
     }
 
-    async fn wait_any_invoice(
+    #[instrument(skip_all)]
+    async fn wait_any_incoming_payment(
         &self,
     ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
+        tracing::info!("Starting stream for fake invoices");
         let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?;
         let receiver_stream = ReceiverStream::new(receiver);
         Ok(Box::pin(receiver_stream.map(|label| label)))
     }
 
+    #[instrument(skip_all)]
     async fn get_payment_quote(
         &self,
-        melt_quote_request: &MeltQuoteBolt11Request,
+        request: &str,
+        unit: &CurrencyUnit,
+        options: Option<MeltOptions>,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        let amount = melt_quote_request.amount_msat()?;
+        let bolt11 = Bolt11Invoice::from_str(request)?;
+
+        let amount_msat = match options {
+            Some(amount) => amount.amount_msat(),
+            None => bolt11
+                .amount_milli_satoshis()
+                .ok_or(Error::UnknownInvoiceAmount)?
+                .into(),
+        };
 
-        let amount = amount / MSAT_IN_SAT.into();
+        let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -142,19 +160,20 @@ impl MintLightning for FakeWallet {
         };
 
         Ok(PaymentQuoteResponse {
-            request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
+            request_lookup_id: bolt11.payment_hash().to_string(),
             amount,
             fee: fee.into(),
             state: MeltQuoteState::Unpaid,
         })
     }
 
-    async fn pay_invoice(
+    #[instrument(skip_all)]
+    async fn make_payment(
         &self,
         melt_quote: mint::MeltQuote,
         _partial_msats: Option<Amount>,
         _max_fee_msats: Option<Amount>,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
 
         let payment_hash = bolt11.payment_hash().to_string();
@@ -185,8 +204,8 @@ impl MintLightning for FakeWallet {
             ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into());
         }
 
-        Ok(PayInvoiceResponse {
-            payment_preimage: Some("".to_string()),
+        Ok(MakePaymentResponse {
+            payment_proof: Some("".to_string()),
             payment_lookup_id: payment_hash,
             status: payment_status,
             total_spent: melt_quote.amount,
@@ -194,16 +213,14 @@ impl MintLightning for FakeWallet {
         })
     }
 
-    async fn create_invoice(
+    #[instrument(skip_all)]
+    async fn create_incoming_payment_request(
         &self,
         amount: Amount,
         _unit: &CurrencyUnit,
         description: String,
-        unix_expiry: u64,
-    ) -> Result<CreateInvoiceResponse, Self::Err> {
-        let time_now = unix_time();
-        assert!(unix_expiry > time_now);
-
+        _unix_expiry: Option<u64>,
+    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         // Since this is fake we just use the amount no matter the unit to create an invoice
         let amount_msat = amount;
 
@@ -229,24 +246,26 @@ impl MintLightning for FakeWallet {
 
         let expiry = invoice.expires_at().map(|t| t.as_secs());
 
-        Ok(CreateInvoiceResponse {
+        Ok(CreateIncomingPaymentResponse {
             request_lookup_id: payment_hash.to_string(),
-            request: invoice,
+            request: invoice.to_string(),
             expiry,
         })
     }
 
-    async fn check_incoming_invoice_status(
+    #[instrument(skip_all)]
+    async fn check_incoming_payment_status(
         &self,
         _request_lookup_id: &str,
     ) -> Result<MintQuoteState, Self::Err> {
         Ok(MintQuoteState::Paid)
     }
 
+    #[instrument(skip_all)]
     async fn check_outgoing_payment(
         &self,
         request_lookup_id: &str,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         // For fake wallet if the state is not explicitly set default to paid
         let states = self.payment_states.lock().await;
         let status = states.get(request_lookup_id).cloned();
@@ -256,20 +275,21 @@ impl MintLightning for FakeWallet {
         let fail_payments = self.failed_payment_check.lock().await;
 
         if fail_payments.contains(request_lookup_id) {
-            return Err(cdk_lightning::Error::InvoicePaymentPending);
+            return Err(cdk_payment::Error::InvoicePaymentPending);
         }
 
-        Ok(PayInvoiceResponse {
-            payment_preimage: Some("".to_string()),
+        Ok(MakePaymentResponse {
+            payment_proof: Some("".to_string()),
             payment_lookup_id: request_lookup_id.to_string(),
             status,
             total_spent: Amount::ZERO,
-            unit: self.get_settings().unit,
+            unit: CurrencyUnit::Msat,
         })
     }
 }
 
 /// Create fake invoice
+#[instrument]
 pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice {
     let private_key = SecretKey::from_slice(
         &[

+ 12 - 10
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -7,7 +7,7 @@ use async_trait::async_trait;
 use bip39::Mnemonic;
 use cdk::amount::SplitTarget;
 use cdk::cdk_database::MintDatabase;
-use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits};
+use cdk::mint::{MintBuilder, MintMeltLimits};
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{
     CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse,
@@ -15,7 +15,7 @@ use cdk::nuts::{
     MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod,
     RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
 };
-use cdk::types::QuoteTTL;
+use cdk::types::{FeeReserve, QuoteTTL};
 use cdk::util::unix_time;
 use cdk::wallet::client::MintConnector;
 use cdk::wallet::Wallet;
@@ -167,20 +167,22 @@ pub async fn create_and_start_test_mint() -> anyhow::Result<Arc<Mint>> {
         percent_fee_reserve: 1.0,
     };
 
-    let ln_fake_backend = Arc::new(FakeWallet::new(
+    let ln_fake_backend = FakeWallet::new(
         fee_reserve.clone(),
         HashMap::default(),
         HashSet::default(),
         0,
-    ));
-
-    mint_builder = mint_builder.add_ln_backend(
-        CurrencyUnit::Sat,
-        PaymentMethod::Bolt11,
-        MintMeltLimits::new(1, 1_000),
-        ln_fake_backend,
     );
 
+    mint_builder = mint_builder
+        .add_ln_backend(
+            CurrencyUnit::Sat,
+            PaymentMethod::Bolt11,
+            MintMeltLimits::new(1, 1_000),
+            Arc::new(ln_fake_backend),
+        )
+        .await?;
+
     let mnemonic = Mnemonic::generate(12)?;
 
     mint_builder = mint_builder

+ 1 - 1
crates/cdk-integration-tests/src/init_regtest.rs

@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::Result;
-use cdk::mint::FeeReserve;
+use cdk::types::FeeReserve;
 use cdk_cln::Cln as CdkCln;
 use cdk_lnd::Lnd as CdkLnd;
 use ln_regtest_rs::bitcoin_client::BitcoinClient;

+ 10 - 8
crates/cdk-integration-tests/tests/mint.rs

@@ -8,14 +8,14 @@ use bip39::Mnemonic;
 use cdk::amount::{Amount, SplitTarget};
 use cdk::cdk_database::MintDatabase;
 use cdk::dhke::construct_proofs;
-use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits, MintQuote};
+use cdk::mint::{MintBuilder, MintMeltLimits, MintQuote};
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{
     CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PaymentMethod,
     PreMintSecrets, ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest,
 };
 use cdk::subscription::{IndexableParams, Params};
-use cdk::types::QuoteTTL;
+use cdk::types::{FeeReserve, QuoteTTL};
 use cdk::util::unix_time;
 use cdk::Mint;
 use cdk_fake_wallet::FakeWallet;
@@ -439,12 +439,14 @@ async fn test_correct_keyset() -> Result<()> {
     let localstore = Arc::new(database);
     mint_builder = mint_builder.with_localstore(localstore.clone());
 
-    mint_builder = mint_builder.add_ln_backend(
-        CurrencyUnit::Sat,
-        PaymentMethod::Bolt11,
-        MintMeltLimits::new(1, 5_000),
-        Arc::new(fake_wallet),
-    );
+    mint_builder = mint_builder
+        .add_ln_backend(
+            CurrencyUnit::Sat,
+            PaymentMethod::Bolt11,
+            MintMeltLimits::new(1, 5_000),
+            Arc::new(fake_wallet),
+        )
+        .await?;
 
     mint_builder = mint_builder
         .with_name("regtest mint".to_string())

+ 176 - 0
crates/cdk-integration-tests/tests/payment_processor.rs

@@ -0,0 +1,176 @@
+//! Tests where we expect the payment processor to respond with an error or pass
+
+use std::env;
+use std::sync::Arc;
+
+use anyhow::{bail, Result};
+use bip39::Mnemonic;
+use cdk::amount::{Amount, SplitTarget};
+use cdk::nuts::nut00::ProofsMethods;
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::Wallet;
+use cdk_fake_wallet::create_fake_invoice;
+use cdk_integration_tests::init_regtest::{get_lnd_dir, get_mint_url, LND_RPC_ADDR};
+use cdk_integration_tests::wait_for_mint_to_be_paid;
+use cdk_sqlite::wallet::memory;
+use ln_regtest_rs::ln_client::{LightningClient, LndClient};
+
+// This is the ln wallet we use to send/receive ln payements as the wallet
+async fn init_lnd_client() -> LndClient {
+    let lnd_dir = get_lnd_dir("one");
+    let cert_file = lnd_dir.join("tls.cert");
+    let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon");
+    LndClient::new(
+        format!("https://{}", LND_RPC_ADDR),
+        cert_file,
+        macaroon_file,
+    )
+    .await
+    .unwrap()
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_regtest_mint() -> Result<()> {
+    let wallet = Wallet::new(
+        &get_mint_url("0"),
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_amount = Amount::from(100);
+
+    let mint_quote = wallet.mint_quote(mint_amount, None).await?;
+
+    assert_eq!(mint_quote.amount, mint_amount);
+
+    let ln_backend = env::var("LN_BACKEND")?;
+
+    if ln_backend != "FAKEWALLET" {
+        let lnd_client = init_lnd_client().await;
+
+        lnd_client.pay_invoice(mint_quote.request).await?;
+    }
+
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+
+    let proofs = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    let mint_amount = proofs.total_amount()?;
+
+    assert!(mint_amount == 100.into());
+
+    Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_regtest_mint_melt() -> Result<()> {
+    let wallet = Wallet::new(
+        &get_mint_url("0"),
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_amount = Amount::from(100);
+
+    let mint_quote = wallet.mint_quote(mint_amount, None).await?;
+
+    assert_eq!(mint_quote.amount, mint_amount);
+
+    let ln_backend = env::var("LN_BACKEND")?;
+    if ln_backend != "FAKEWALLET" {
+        let lnd_client = init_lnd_client().await;
+
+        lnd_client.pay_invoice(mint_quote.request).await?;
+    }
+
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+
+    let proofs = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    let mint_amount = proofs.total_amount()?;
+
+    assert!(mint_amount == 100.into());
+
+    let invoice = if ln_backend != "FAKEWALLET" {
+        let lnd_client = init_lnd_client().await;
+        lnd_client.create_invoice(Some(50)).await?
+    } else {
+        create_fake_invoice(50000, "".to_string()).to_string()
+    };
+
+    let melt_quote = wallet.melt_quote(invoice, None).await?;
+
+    wallet.melt(&melt_quote.id).await?;
+
+    Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_pay_invoice_twice() -> Result<()> {
+    let ln_backend = env::var("LN_BACKEND")?;
+    if ln_backend == "FAKEWALLET" {
+        // We can only preform this test on regtest backends as fake wallet just marks the quote as paid
+        return Ok(());
+    }
+
+    let seed = Mnemonic::generate(12)?.to_seed_normalized("");
+    let wallet = Wallet::new(
+        &get_mint_url("0"),
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &seed,
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    let lnd_client = init_lnd_client().await;
+
+    lnd_client.pay_invoice(mint_quote.request).await?;
+
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+
+    let proofs = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    let mint_amount = proofs.total_amount()?;
+
+    assert_eq!(mint_amount, 100.into());
+
+    let invoice = lnd_client.create_invoice(Some(25)).await?;
+
+    let melt_quote = wallet.melt_quote(invoice.clone(), None).await?;
+
+    let melt = wallet.melt(&melt_quote.id).await.unwrap();
+
+    let melt_two = wallet.melt_quote(invoice, None).await?;
+
+    let melt_two = wallet.melt(&melt_two.id).await;
+
+    match melt_two {
+        Err(err) => match err {
+            cdk::Error::RequestAlreadyPaid => (),
+            err => {
+                bail!("Wrong invoice already paid: {}", err.to_string());
+            }
+        },
+        Ok(_) => {
+            bail!("Should not have allowed second payment");
+        }
+    }
+
+    let balance = wallet.total_balance().await?;
+
+    assert_eq!(balance, (Amount::from(100) - melt.fee_paid - melt.amount));
+
+    Ok(())
+}

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

@@ -21,3 +21,4 @@ tokio-util.workspace = true
 tracing.workspace = true
 thiserror.workspace = true
 lnbits-rs = "0.4.0"
+serde_json.workspace = true

+ 1 - 1
crates/cdk-lnbits/src/error.rs

@@ -19,7 +19,7 @@ pub enum Error {
     Anyhow(#[from] anyhow::Error),
 }
 
-impl From<Error> for cdk::cdk_lightning::Error {
+impl From<Error> for cdk::cdk_payment::Error {
     fn from(e: Error) -> Self {
         Self::Lightning(Box::new(e))
     }

+ 54 - 35
crates/cdk-lnbits/src/lib.rs

@@ -4,6 +4,7 @@
 #![warn(rustdoc::bare_urls)]
 
 use std::pin::Pin;
+use std::str::FromStr;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 
@@ -11,11 +12,12 @@ use anyhow::anyhow;
 use async_trait::async_trait;
 use axum::Router;
 use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
-use cdk::cdk_lightning::{
-    self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
+use cdk::cdk_payment::{
+    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
+    PaymentQuoteResponse,
 };
-use cdk::mint::FeeReserve;
-use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
+use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
+use cdk::types::FeeReserve;
 use cdk::util::unix_time;
 use cdk::{mint, Bolt11Invoice};
 use error::Error;
@@ -23,6 +25,7 @@ use futures::stream::StreamExt;
 use futures::Stream;
 use lnbits_rs::api::invoice::CreateInvoiceRequest;
 use lnbits_rs::LNBitsClient;
+use serde_json::Value;
 use tokio::sync::Mutex;
 use tokio_util::sync::CancellationToken;
 
@@ -37,6 +40,7 @@ pub struct LNbits {
     webhook_url: String,
     wait_invoice_cancel_token: CancellationToken,
     wait_invoice_is_active: Arc<AtomicBool>,
+    settings: Bolt11Settings,
 }
 
 impl LNbits {
@@ -59,20 +63,21 @@ impl LNbits {
             webhook_url,
             wait_invoice_cancel_token: CancellationToken::new(),
             wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
+            settings: Bolt11Settings {
+                mpp: false,
+                unit: CurrencyUnit::Sat,
+                invoice_description: true,
+            },
         })
     }
 }
 
 #[async_trait]
-impl MintLightning for LNbits {
-    type Err = cdk_lightning::Error;
+impl MintPayment for LNbits {
+    type Err = cdk_payment::Error;
 
-    fn get_settings(&self) -> Settings {
-        Settings {
-            mpp: false,
-            unit: CurrencyUnit::Sat,
-            invoice_description: true,
-        }
+    async fn get_settings(&self) -> Result<Value, Self::Err> {
+        Ok(serde_json::to_value(&self.settings)?)
     }
 
     fn is_wait_invoice_active(&self) -> bool {
@@ -83,7 +88,7 @@ impl MintLightning for LNbits {
         self.wait_invoice_cancel_token.cancel()
     }
 
-    async fn wait_any_invoice(
+    async fn wait_any_incoming_payment(
         &self,
     ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
         let receiver = self
@@ -145,15 +150,30 @@ impl MintLightning for LNbits {
 
     async fn get_payment_quote(
         &self,
-        melt_quote_request: &MeltQuoteBolt11Request,
+        request: &str,
+        unit: &CurrencyUnit,
+        options: Option<MeltOptions>,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        if melt_quote_request.unit != CurrencyUnit::Sat {
+        if unit != &CurrencyUnit::Sat {
             return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
         }
 
-        let amount = melt_quote_request.amount_msat()?;
+        let bolt11 = Bolt11Invoice::from_str(request)?;
 
-        let amount = amount / MSAT_IN_SAT.into();
+        let amount_msat = match options {
+            Some(amount) => {
+                if matches!(amount, MeltOptions::Mpp { mpp: _ }) {
+                    return Err(cdk_payment::Error::UnsupportedPaymentOption);
+                }
+                amount.amount_msat()
+            }
+            None => bolt11
+                .amount_milli_satoshis()
+                .ok_or(Error::UnknownInvoiceAmount)?
+                .into(),
+        };
+
+        let amount = amount_msat / MSAT_IN_SAT.into();
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -166,19 +186,19 @@ impl MintLightning for LNbits {
         };
 
         Ok(PaymentQuoteResponse {
-            request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
+            request_lookup_id: bolt11.payment_hash().to_string(),
             amount,
             fee: fee.into(),
             state: MeltQuoteState::Unpaid,
         })
     }
 
-    async fn pay_invoice(
+    async fn make_payment(
         &self,
         melt_quote: mint::MeltQuote,
         _partial_msats: Option<Amount>,
         _max_fee_msats: Option<Amount>,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let pay_response = self
             .lnbits_api
             .pay_invoice(&melt_quote.request, None)
@@ -212,36 +232,35 @@ impl MintLightning for LNbits {
             .unsigned_abs(),
         );
 
-        Ok(PayInvoiceResponse {
+        Ok(MakePaymentResponse {
             payment_lookup_id: pay_response.payment_hash,
-            payment_preimage: Some(invoice_info.payment_hash),
+            payment_proof: Some(invoice_info.payment_hash),
             status,
             total_spent,
             unit: CurrencyUnit::Sat,
         })
     }
 
-    async fn create_invoice(
+    async fn create_incoming_payment_request(
         &self,
         amount: Amount,
         unit: &CurrencyUnit,
         description: String,
-        unix_expiry: u64,
-    ) -> Result<CreateInvoiceResponse, Self::Err> {
+        unix_expiry: Option<u64>,
+    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         if unit != &CurrencyUnit::Sat {
             return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
         }
 
         let time_now = unix_time();
-        assert!(unix_expiry > time_now);
 
-        let expiry = unix_expiry - time_now;
+        let expiry = unix_expiry.map(|t| t - time_now);
 
         let invoice_request = CreateInvoiceRequest {
             amount: to_unit(amount, unit, &CurrencyUnit::Sat)?.into(),
             memo: Some(description),
             unit: unit.to_string(),
-            expiry: Some(expiry),
+            expiry,
             webhook: Some(self.webhook_url.clone()),
             internal: None,
             out: false,
@@ -260,14 +279,14 @@ impl MintLightning for LNbits {
         let request: Bolt11Invoice = create_invoice_response.payment_request.parse()?;
         let expiry = request.expires_at().map(|t| t.as_secs());
 
-        Ok(CreateInvoiceResponse {
+        Ok(CreateIncomingPaymentResponse {
             request_lookup_id: create_invoice_response.payment_hash,
-            request,
+            request: request.to_string(),
             expiry,
         })
     }
 
-    async fn check_incoming_invoice_status(
+    async fn check_incoming_payment_status(
         &self,
         payment_hash: &str,
     ) -> Result<MintQuoteState, Self::Err> {
@@ -292,7 +311,7 @@ impl MintLightning for LNbits {
     async fn check_outgoing_payment(
         &self,
         payment_hash: &str,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let payment = self
             .lnbits_api
             .get_payment_info(payment_hash)
@@ -303,15 +322,15 @@ impl MintLightning for LNbits {
                 Self::Err::Anyhow(anyhow!("Could not check invoice status"))
             })?;
 
-        let pay_response = PayInvoiceResponse {
+        let pay_response = MakePaymentResponse {
             payment_lookup_id: payment.details.payment_hash,
-            payment_preimage: Some(payment.preimage),
+            payment_proof: Some(payment.preimage),
             status: lnbits_to_melt_status(&payment.details.status, payment.details.pending),
             total_spent: Amount::from(
                 payment.details.amount.unsigned_abs()
                     + payment.details.fee.unsigned_abs() / MSAT_IN_SAT,
             ),
-            unit: self.get_settings().unit,
+            unit: self.settings.unit.clone(),
         };
 
         Ok(pay_response)

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

@@ -18,3 +18,4 @@ tokio.workspace = true
 tokio-util.workspace = true
 tracing.workspace = true
 thiserror.workspace = true
+serde_json.workspace = true

+ 1 - 1
crates/cdk-lnd/src/error.rs

@@ -38,7 +38,7 @@ pub enum Error {
     InvalidConfig(String),
 }
 
-impl From<Error> for cdk::cdk_lightning::Error {
+impl From<Error> for cdk::cdk_payment::Error {
     fn from(e: Error) -> Self {
         Self::Lightning(Box::new(e))
     }

+ 58 - 48
crates/cdk-lnd/src/lib.rs

@@ -14,13 +14,14 @@ use std::sync::Arc;
 use anyhow::anyhow;
 use async_trait::async_trait;
 use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
-use cdk::cdk_lightning::{
-    self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
+use cdk::cdk_payment::{
+    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
+    PaymentQuoteResponse,
 };
-use cdk::mint::FeeReserve;
-use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
+use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
 use cdk::secp256k1::hashes::Hash;
-use cdk::util::{hex, unix_time};
+use cdk::types::FeeReserve;
+use cdk::util::hex;
 use cdk::{mint, Bolt11Invoice};
 use error::Error;
 use fedimint_tonic_lnd::lnrpc::fee_limit::Limit;
@@ -45,6 +46,7 @@ pub struct Lnd {
     fee_reserve: FeeReserve,
     wait_invoice_cancel_token: CancellationToken,
     wait_invoice_is_active: Arc<AtomicBool>,
+    settings: Bolt11Settings,
 }
 
 impl Lnd {
@@ -96,21 +98,22 @@ impl Lnd {
             fee_reserve,
             wait_invoice_cancel_token: CancellationToken::new(),
             wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
+            settings: Bolt11Settings {
+                mpp: true,
+                unit: CurrencyUnit::Msat,
+                invoice_description: true,
+            },
         })
     }
 }
 
 #[async_trait]
-impl MintLightning for Lnd {
-    type Err = cdk_lightning::Error;
+impl MintPayment for Lnd {
+    type Err = cdk_payment::Error;
 
     #[instrument(skip_all)]
-    fn get_settings(&self) -> Settings {
-        Settings {
-            mpp: true,
-            unit: CurrencyUnit::Msat,
-            invoice_description: true,
-        }
+    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
+        Ok(serde_json::to_value(&self.settings)?)
     }
 
     #[instrument(skip_all)]
@@ -124,7 +127,7 @@ impl MintLightning for Lnd {
     }
 
     #[instrument(skip_all)]
-    async fn wait_any_invoice(
+    async fn wait_any_incoming_payment(
         &self,
     ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
         let mut client =
@@ -183,7 +186,7 @@ impl MintLightning for Lnd {
                     }, // End of stream
                     Err(err) => {
                     is_active.store(false, Ordering::SeqCst);
-                    tracing::warn!("Encounrdered error in LND invoice stream. Stream ending");
+                    tracing::warn!("Encountered error in LND invoice stream. Stream ending");
                     tracing::error!("{:?}", err);
                     None
 
@@ -199,11 +202,21 @@ impl MintLightning for Lnd {
     #[instrument(skip_all)]
     async fn get_payment_quote(
         &self,
-        melt_quote_request: &MeltQuoteBolt11Request,
+        request: &str,
+        unit: &CurrencyUnit,
+        options: Option<MeltOptions>,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        let amount = melt_quote_request.amount_msat()?;
+        let bolt11 = Bolt11Invoice::from_str(request)?;
 
-        let amount = amount / MSAT_IN_SAT.into();
+        let amount_msat = match options {
+            Some(amount) => amount.amount_msat(),
+            None => bolt11
+                .amount_milli_satoshis()
+                .ok_or(Error::UnknownInvoiceAmount)?
+                .into(),
+        };
+
+        let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -216,7 +229,7 @@ impl MintLightning for Lnd {
         };
 
         Ok(PaymentQuoteResponse {
-            request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
+            request_lookup_id: bolt11.payment_hash().to_string(),
             amount,
             fee: fee.into(),
             state: MeltQuoteState::Unpaid,
@@ -224,12 +237,12 @@ impl MintLightning for Lnd {
     }
 
     #[instrument(skip_all)]
-    async fn pay_invoice(
+    async fn make_payment(
         &self,
         melt_quote: mint::MeltQuote,
         partial_amount: Option<Amount>,
         max_fee: Option<Amount>,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let payment_request = melt_quote.request;
         let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
 
@@ -347,9 +360,9 @@ impl MintLightning for Lnd {
                     total_amt = (route.total_amt_msat / 1000) as u64;
                 }
 
-                Ok(PayInvoiceResponse {
+                Ok(MakePaymentResponse {
                     payment_lookup_id: hex::encode(payment_hash),
-                    payment_preimage,
+                    payment_proof: payment_preimage,
                     status,
                     total_spent: total_amt.into(),
                     unit: CurrencyUnit::Sat,
@@ -393,9 +406,9 @@ impl MintLightning for Lnd {
                     ),
                 };
 
-                Ok(PayInvoiceResponse {
+                Ok(MakePaymentResponse {
                     payment_lookup_id: hex::encode(payment_response.payment_hash),
-                    payment_preimage,
+                    payment_proof: payment_preimage,
                     status,
                     total_spent: total_amount.into(),
                     unit: CurrencyUnit::Sat,
@@ -405,16 +418,13 @@ impl MintLightning for Lnd {
     }
 
     #[instrument(skip(self, description))]
-    async fn create_invoice(
+    async fn create_incoming_payment_request(
         &self,
         amount: Amount,
         unit: &CurrencyUnit,
         description: String,
-        unix_expiry: u64,
-    ) -> Result<CreateInvoiceResponse, Self::Err> {
-        let time_now = unix_time();
-        assert!(unix_expiry > time_now);
-
+        unix_expiry: Option<u64>,
+    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
 
         let invoice_request = fedimint_tonic_lnd::lnrpc::Invoice {
@@ -435,15 +445,15 @@ impl MintLightning for Lnd {
 
         let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
 
-        Ok(CreateInvoiceResponse {
+        Ok(CreateIncomingPaymentResponse {
             request_lookup_id: bolt11.payment_hash().to_string(),
-            request: bolt11,
-            expiry: Some(unix_expiry),
+            request: bolt11.to_string(),
+            expiry: unix_expiry,
         })
     }
 
     #[instrument(skip(self))]
-    async fn check_incoming_invoice_status(
+    async fn check_incoming_payment_status(
         &self,
         request_lookup_id: &str,
     ) -> Result<MintQuoteState, Self::Err> {
@@ -479,7 +489,7 @@ impl MintLightning for Lnd {
     async fn check_outgoing_payment(
         &self,
         payment_hash: &str,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let track_request = fedimint_tonic_lnd::routerrpc::TrackPaymentRequest {
             payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
             no_inflight_updates: true,
@@ -498,15 +508,15 @@ impl MintLightning for Lnd {
             Err(err) => {
                 let err_code = err.code();
                 if err_code == Code::NotFound {
-                    return Ok(PayInvoiceResponse {
+                    return Ok(MakePaymentResponse {
                         payment_lookup_id: payment_hash.to_string(),
-                        payment_preimage: None,
+                        payment_proof: None,
                         status: MeltQuoteState::Unknown,
                         total_spent: Amount::ZERO,
-                        unit: self.get_settings().unit,
+                        unit: self.settings.unit.clone(),
                     });
                 } else {
-                    return Err(cdk_lightning::Error::UnknownPaymentState);
+                    return Err(cdk_payment::Error::UnknownPaymentState);
                 }
             }
         };
@@ -517,20 +527,20 @@ impl MintLightning for Lnd {
                     let status = update.status();
 
                     let response = match status {
-                        PaymentStatus::Unknown => PayInvoiceResponse {
+                        PaymentStatus::Unknown => MakePaymentResponse {
                             payment_lookup_id: payment_hash.to_string(),
-                            payment_preimage: Some(update.payment_preimage),
+                            payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Unknown,
                             total_spent: Amount::ZERO,
-                            unit: self.get_settings().unit,
+                            unit: self.settings.unit.clone(),
                         },
                         PaymentStatus::InFlight => {
                             // Continue waiting for the next update
                             continue;
                         }
-                        PaymentStatus::Succeeded => PayInvoiceResponse {
+                        PaymentStatus::Succeeded => MakePaymentResponse {
                             payment_lookup_id: payment_hash.to_string(),
-                            payment_preimage: Some(update.payment_preimage),
+                            payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Paid,
                             total_spent: Amount::from(
                                 (update
@@ -541,12 +551,12 @@ impl MintLightning for Lnd {
                             ),
                             unit: CurrencyUnit::Sat,
                         },
-                        PaymentStatus::Failed => PayInvoiceResponse {
+                        PaymentStatus::Failed => MakePaymentResponse {
                             payment_lookup_id: payment_hash.to_string(),
-                            payment_preimage: Some(update.payment_preimage),
+                            payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Failed,
                             total_spent: Amount::ZERO,
-                            unit: self.get_settings().unit,
+                            unit: self.settings.unit.clone(),
                         },
                     };
 

+ 3 - 7
crates/cdk-mint-rpc/Cargo.toml

@@ -19,20 +19,16 @@ cdk = { workspace = true, features = [
     "mint",
 ] }
 clap.workspace = true
-tonic = { version = "0.12.3", features = [
-    "channel",
-    "tls",
-    "tls-webpki-roots",
-] }
+tonic.workspace = true
 tracing.workspace = true
 tracing-subscriber.workspace = true
 tokio.workspace = true
 serde_json.workspace = true
 serde.workspace = true
 thiserror.workspace = true
-prost = "0.13.1"
+prost.workspace = true
 home.workspace = true
 
 
 [build-dependencies]
-tonic-build = "0.12"
+tonic-build.workspace = true

+ 9 - 7
crates/cdk-mintd/Cargo.toml

@@ -10,18 +10,19 @@ description = "CDK mint binary"
 rust-version = "1.75.0"
 
 [features]
-default = ["management-rpc", "cln", "lnd", "lnbits", "fakewallet"]
+default = ["management-rpc", "cln", "lnd", "lnbits", "fakewallet", "grpc-processor"]
 # Ensure at least one lightning backend is enabled
-swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"]
-redis = ["cdk-axum/redis"]
 management-rpc = ["cdk-mint-rpc"]
-# MSRV is not commited to with redb enabled
-redb = ["dep:cdk-redb"]
-sqlcipher = ["cdk-sqlite/sqlcipher"]
 cln = ["dep:cdk-cln"]
 lnd = ["dep:cdk-lnd"]
 lnbits = ["dep:cdk-lnbits"]
 fakewallet = ["dep:cdk-fake-wallet"]
+grpc-processor = ["dep:cdk-payment-processor"]
+sqlcipher = ["cdk-sqlite/sqlcipher"]
+# MSRV is not commited to with redb enabled
+redb = ["dep:cdk-redb"]
+swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"]
+redis = ["cdk-axum/redis"]
 
 [dependencies]
 anyhow.workspace = true
@@ -42,6 +43,7 @@ cdk-lnd = { workspace = true, optional = true }
 cdk-fake-wallet = { workspace = true, optional = true }
 cdk-axum.workspace = true
 cdk-mint-rpc = { workspace = true, optional = true }
+cdk-payment-processor = { workspace = true, optional = true }
 config = { version = "0.13.3", features = ["toml"] }
 clap.workspace = true
 bitcoin.workspace = true
@@ -54,7 +56,7 @@ bip39.workspace = true
 tower-http = { workspace = true, features = ["compression-full", "decompression-full"] }
 tower = "0.5.2"
 lightning-invoice.workspace = true
-home = "0.5.5"
+home.workspace = true
 url.workspace = true
 utoipa = { workspace = true, optional = true }
 utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true }

+ 7 - 0
crates/cdk-mintd/example.config.toml

@@ -91,3 +91,10 @@ reserve_fee_min = 4
 # reserve_fee_min = 1
 # min_delay_time = 1
 # max_delay_time = 3
+
+# [grpc_processor]
+# gRPC Payment Processor configuration
+# supported_units = ["sat"]
+# addr = "127.0.0.1"
+# port = 50051
+# tls_dir = "/path/to/tls"

+ 21 - 3
crates/cdk-mintd/src/config.rs

@@ -1,9 +1,7 @@
 use std::path::PathBuf;
 
 use bitcoin::hashes::{sha256, Hash};
-#[cfg(feature = "fakewallet")]
-use cdk::nuts::CurrencyUnit;
-use cdk::nuts::PublicKey;
+use cdk::nuts::{CurrencyUnit, PublicKey};
 use cdk::Amount;
 use cdk_axum::cache;
 use config::{Config, ConfigError, File};
@@ -56,6 +54,8 @@ pub enum LnBackend {
     FakeWallet,
     #[cfg(feature = "lnd")]
     Lnd,
+    #[cfg(feature = "grpc-processor")]
+    GrpcProcessor,
 }
 
 impl std::str::FromStr for LnBackend {
@@ -71,6 +71,8 @@ impl std::str::FromStr for LnBackend {
             "fakewallet" => Ok(LnBackend::FakeWallet),
             #[cfg(feature = "lnd")]
             "lnd" => Ok(LnBackend::Lnd),
+            #[cfg(feature = "grpc-processor")]
+            "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
             _ => Err(format!("Unknown Lightning backend: {}", s)),
         }
     }
@@ -166,6 +168,14 @@ fn default_max_delay_time() -> u64 {
 }
 
 #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
+pub struct GrpcProcessor {
+    pub supported_units: Vec<CurrencyUnit>,
+    pub addr: String,
+    pub port: u16,
+    pub tls_dir: Option<PathBuf>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
 #[serde(rename_all = "lowercase")]
 pub enum DatabaseEngine {
     #[default]
@@ -206,6 +216,7 @@ pub struct Settings {
     pub lnd: Option<Lnd>,
     #[cfg(feature = "fakewallet")]
     pub fake_wallet: Option<FakeWallet>,
+    pub grpc_processor: Option<GrpcProcessor>,
     pub database: Database,
     #[cfg(feature = "management-rpc")]
     pub mint_management_rpc: Option<MintManagementRpc>,
@@ -313,6 +324,13 @@ impl Settings {
                 settings.fake_wallet.is_some(),
                 "FakeWallet backend requires a valid config."
             ),
+            #[cfg(feature = "grpc-processor")]
+            LnBackend::GrpcProcessor => {
+                assert!(
+                    settings.grpc_processor.is_some(),
+                    "GRPC backend requires a valid config."
+                )
+            }
         }
 
         Ok(settings)

+ 44 - 0
crates/cdk-mintd/src/env_vars/grpc_processor.rs

@@ -0,0 +1,44 @@
+//! gRPC Payment Processor environment variables
+
+use std::env;
+
+use cdk::nuts::CurrencyUnit;
+
+use crate::config::GrpcProcessor;
+
+// gRPC Payment Processor environment variables
+pub const ENV_GRPC_PROCESSOR_SUPPORTED_UNITS: &str =
+    "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_SUPPORTED_UNITS";
+pub const ENV_GRPC_PROCESSOR_ADDRESS: &str = "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_ADDRESS";
+pub const ENV_GRPC_PROCESSOR_PORT: &str = "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_PORT";
+pub const ENV_GRPC_PROCESSOR_TLS_DIR: &str = "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_TLS_DIR";
+
+impl GrpcProcessor {
+    pub fn from_env(mut self) -> Self {
+        if let Ok(units_str) = env::var(ENV_GRPC_PROCESSOR_SUPPORTED_UNITS) {
+            if let Ok(units) = units_str
+                .split(',')
+                .map(|s| s.trim().parse())
+                .collect::<Result<Vec<CurrencyUnit>, _>>()
+            {
+                self.supported_units = units;
+            }
+        }
+
+        if let Ok(addr) = env::var(ENV_GRPC_PROCESSOR_ADDRESS) {
+            self.addr = addr;
+        }
+
+        if let Ok(port) = env::var(ENV_GRPC_PROCESSOR_PORT) {
+            if let Ok(port) = port.parse() {
+                self.port = port;
+            }
+        }
+
+        if let Ok(tls_dir) = env::var(ENV_GRPC_PROCESSOR_TLS_DIR) {
+            self.tls_dir = Some(tls_dir.into());
+        }
+
+        self
+    }
+}

+ 2 - 0
crates/cdk-mintd/src/env_vars/ln.rs

@@ -18,6 +18,8 @@ impl Ln {
         if let Ok(backend_str) = env::var(ENV_LN_BACKEND) {
             if let Ok(backend) = backend_str.parse() {
                 self.ln_backend = backend;
+            } else {
+                tracing::warn!("Unknow payment backend set in env var will attempt to use config file. {backend_str}");
             }
         }
 

+ 9 - 0
crates/cdk-mintd/src/env_vars/mod.rs

@@ -12,6 +12,8 @@ mod mint_info;
 mod cln;
 #[cfg(feature = "fakewallet")]
 mod fake_wallet;
+#[cfg(feature = "grpc-processor")]
+mod grpc_processor;
 #[cfg(feature = "lnbits")]
 mod lnbits;
 #[cfg(feature = "lnd")]
@@ -28,6 +30,8 @@ pub use cln::*;
 pub use common::*;
 #[cfg(feature = "fakewallet")]
 pub use fake_wallet::*;
+#[cfg(feature = "grpc-processor")]
+pub use grpc_processor::*;
 pub use ln::*;
 #[cfg(feature = "lnbits")]
 pub use lnbits::*;
@@ -77,6 +81,11 @@ impl Settings {
             LnBackend::Lnd => {
                 self.lnd = Some(self.lnd.clone().unwrap_or_default().from_env());
             }
+            #[cfg(feature = "grpc-processor")]
+            LnBackend::GrpcProcessor => {
+                self.grpc_processor =
+                    Some(self.grpc_processor.clone().unwrap_or_default().from_env());
+            }
             LnBackend::None => bail!("Ln backend must be set"),
             #[allow(unreachable_patterns)]
             _ => bail!("Selected Ln backend is not enabled in this build"),

+ 102 - 37
crates/cdk-mintd/src/main.rs

@@ -19,11 +19,19 @@ use cdk::mint::{MintBuilder, MintMeltLimits};
     feature = "cln",
     feature = "lnbits",
     feature = "lnd",
-    feature = "fakewallet"
+    feature = "fakewallet",
+    feature = "grpc-processor"
 ))]
 use cdk::nuts::nut17::SupportedMethods;
 use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path};
-use cdk::nuts::{ContactInfo, CurrencyUnit, MintVersion, PaymentMethod};
+#[cfg(any(
+    feature = "cln",
+    feature = "lnbits",
+    feature = "lnd",
+    feature = "fakewallet"
+))]
+use cdk::nuts::CurrencyUnit;
+use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod};
 use cdk::types::QuoteTTL;
 use cdk_axum::cache::HttpCache;
 #[cfg(feature = "management-rpc")]
@@ -52,10 +60,11 @@ const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION")
     feature = "cln",
     feature = "lnbits",
     feature = "lnd",
-    feature = "fakewallet"
+    feature = "fakewallet",
+    feature = "grpc-processor"
 )))]
 compile_error!(
-    "At least one lightning backend feature must be enabled: cln, lnbits, lnd, or fakewallet"
+    "At least one lightning backend feature must be enabled: cln, lnbits, lnd, fakewallet, or grpc-processor"
 );
 
 #[tokio::main]
@@ -169,6 +178,8 @@ async fn main() -> anyhow::Result<()> {
         melt_max: settings.ln.max_melt,
     };
 
+    tracing::debug!("Ln backendd: {:?}", settings.ln.ln_backend);
+
     match settings.ln.ln_backend {
         #[cfg(feature = "cln")]
         LnBackend::Cln => {
@@ -182,12 +193,14 @@ async fn main() -> anyhow::Result<()> {
                 .await?;
             let cln = Arc::new(cln);
 
-            mint_builder = mint_builder.add_ln_backend(
-                CurrencyUnit::Sat,
-                PaymentMethod::Bolt11,
-                mint_melt_limits,
-                cln.clone(),
-            );
+            mint_builder = mint_builder
+                .add_ln_backend(
+                    CurrencyUnit::Sat,
+                    PaymentMethod::Bolt11,
+                    mint_melt_limits,
+                    cln.clone(),
+                )
+                .await?;
 
             let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat);
 
@@ -200,12 +213,15 @@ async fn main() -> anyhow::Result<()> {
                 .setup(&mut ln_routers, &settings, CurrencyUnit::Sat)
                 .await?;
 
-            mint_builder = mint_builder.add_ln_backend(
-                CurrencyUnit::Sat,
-                PaymentMethod::Bolt11,
-                mint_melt_limits,
-                Arc::new(lnbits),
-            );
+            mint_builder = mint_builder
+                .add_ln_backend(
+                    CurrencyUnit::Sat,
+                    PaymentMethod::Bolt11,
+                    mint_melt_limits,
+                    Arc::new(lnbits),
+                )
+                .await?;
+
             let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat);
 
             mint_builder = mint_builder.add_supported_websockets(nut17_supported);
@@ -217,12 +233,14 @@ async fn main() -> anyhow::Result<()> {
                 .setup(&mut ln_routers, &settings, CurrencyUnit::Msat)
                 .await?;
 
-            mint_builder = mint_builder.add_ln_backend(
-                CurrencyUnit::Sat,
-                PaymentMethod::Bolt11,
-                mint_melt_limits,
-                Arc::new(lnd),
-            );
+            mint_builder = mint_builder
+                .add_ln_backend(
+                    CurrencyUnit::Sat,
+                    PaymentMethod::Bolt11,
+                    mint_melt_limits,
+                    Arc::new(lnd),
+                )
+                .await?;
 
             let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat);
 
@@ -231,27 +249,72 @@ async fn main() -> anyhow::Result<()> {
         #[cfg(feature = "fakewallet")]
         LnBackend::FakeWallet => {
             let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined");
+            tracing::info!("Using fake wallet: {:?}", fake_wallet);
 
             for unit in fake_wallet.clone().supported_units {
                 let fake = fake_wallet
                     .setup(&mut ln_routers, &settings, CurrencyUnit::Sat)
-                    .await?;
+                    .await
+                    .expect("hhh");
 
                 let fake = Arc::new(fake);
 
-                mint_builder = mint_builder.add_ln_backend(
-                    unit.clone(),
-                    PaymentMethod::Bolt11,
-                    mint_melt_limits,
-                    fake.clone(),
-                );
+                mint_builder = mint_builder
+                    .add_ln_backend(
+                        unit.clone(),
+                        PaymentMethod::Bolt11,
+                        mint_melt_limits,
+                        fake.clone(),
+                    )
+                    .await?;
 
                 let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, unit);
 
                 mint_builder = mint_builder.add_supported_websockets(nut17_supported);
             }
         }
-        LnBackend::None => bail!("Ln backend must be set"),
+        #[cfg(feature = "grpc-processor")]
+        LnBackend::GrpcProcessor => {
+            let grpc_processor = settings
+                .clone()
+                .grpc_processor
+                .expect("grpc processor config defined");
+
+            tracing::info!(
+                "Attempting to start with gRPC payment processor at {}:{}.",
+                grpc_processor.addr,
+                grpc_processor.port
+            );
+
+            tracing::info!("{:?}", grpc_processor);
+
+            for unit in grpc_processor.clone().supported_units {
+                tracing::debug!("Adding unit: {:?}", unit);
+
+                let processor = grpc_processor
+                    .setup(&mut ln_routers, &settings, unit.clone())
+                    .await?;
+
+                mint_builder = mint_builder
+                    .add_ln_backend(
+                        unit.clone(),
+                        PaymentMethod::Bolt11,
+                        mint_melt_limits,
+                        Arc::new(processor),
+                    )
+                    .await?;
+
+                let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, unit);
+                mint_builder = mint_builder.add_supported_websockets(nut17_supported);
+            }
+        }
+        LnBackend::None => {
+            tracing::error!(
+                "Pyament backend was not set or feature disabled. {:?}",
+                settings.ln.ln_backend
+            );
+            bail!("Ln backend must be")
+        }
     };
 
     if let Some(long_description) = &settings.mint_info.description_long {
@@ -300,6 +363,8 @@ async fn main() -> anyhow::Result<()> {
 
     let mint = mint_builder.build().await?;
 
+    tracing::debug!("Mint built from builder.");
+
     let mint = Arc::new(mint);
 
     // Check the status of any mint quotes that are pending
@@ -425,6 +490,13 @@ async fn main() -> anyhow::Result<()> {
     Ok(())
 }
 
+async fn shutdown_signal() {
+    tokio::signal::ctrl_c()
+        .await
+        .expect("failed to install CTRL+C handler");
+    tracing::info!("Shutdown signal received");
+}
+
 fn work_dir() -> Result<PathBuf> {
     let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
     let dir = home_dir.join(".cdk-mintd");
@@ -433,10 +505,3 @@ fn work_dir() -> Result<PathBuf> {
 
     Ok(dir)
 }
-
-async fn shutdown_signal() {
-    tokio::signal::ctrl_c()
-        .await
-        .expect("failed to install CTRL+C handler");
-    tracing::info!("Shutdown signal received");
-}

+ 29 - 3
crates/cdk-mintd/src/setup.rs

@@ -11,11 +11,17 @@ use async_trait::async_trait;
 use axum::Router;
 #[cfg(feature = "fakewallet")]
 use bip39::rand::{thread_rng, Rng};
-use cdk::cdk_lightning::MintLightning;
-use cdk::mint::FeeReserve;
+use cdk::cdk_payment::MintPayment;
 #[cfg(feature = "lnbits")]
 use cdk::mint_url::MintUrl;
 use cdk::nuts::CurrencyUnit;
+#[cfg(any(
+    feature = "lnbits",
+    feature = "cln",
+    feature = "lnd",
+    feature = "fakewallet"
+))]
+use cdk::types::FeeReserve;
 #[cfg(feature = "lnbits")]
 use tokio::sync::Mutex;
 
@@ -30,7 +36,7 @@ pub trait LnBackendSetup {
         routers: &mut Vec<Router>,
         settings: &Settings,
         unit: CurrencyUnit,
-    ) -> anyhow::Result<impl MintLightning>;
+    ) -> anyhow::Result<impl MintPayment>;
 }
 
 #[cfg(feature = "cln")]
@@ -162,3 +168,23 @@ impl LnBackendSetup for config::FakeWallet {
         Ok(fake_wallet)
     }
 }
+
+#[cfg(feature = "grpc-processor")]
+#[async_trait]
+impl LnBackendSetup for config::GrpcProcessor {
+    async fn setup(
+        &self,
+        _routers: &mut Vec<Router>,
+        _settings: &Settings,
+        _unit: CurrencyUnit,
+    ) -> anyhow::Result<cdk_payment_processor::PaymentProcessorClient> {
+        let payment_processor = cdk_payment_processor::PaymentProcessorClient::new(
+            &self.addr,
+            self.port,
+            self.tls_dir.clone(),
+        )
+        .await?;
+
+        Ok(payment_processor)
+    }
+}

+ 65 - 0
crates/cdk-payment-processor/Cargo.toml

@@ -0,0 +1,65 @@
+[package]
+name = "cdk-payment-processor"
+version = "0.7.1"
+edition = "2021"
+authors = ["CDK Developers"]
+description = "CDK payment processor"
+homepage = "https://github.com/cashubtc/cdk"
+repository = "https://github.com/cashubtc/cdk.git"
+rust-version = "1.75.0"                     # MSRV
+license = "MIT"
+
+[[bin]]
+name = "cdk-payment-processor"
+path = "src/bin/payment_processor.rs"
+
+[features]
+default = ["cln", "fake", "lnd"]
+bench = []
+cln = ["dep:cdk-cln"]
+fake = ["dep:cdk-fake-wallet"]
+lnd = ["dep:cdk-lnd"]
+
+[dependencies]
+anyhow.workspace = true
+async-trait.workspace = true
+bitcoin.workspace = true
+cdk-common = { workspace = true, features = ["mint"] }
+cdk-cln = { workspace = true, optional = true }
+cdk-lnd = { workspace = true, optional = true }
+cdk-fake-wallet = { workspace = true, optional = true }
+serde.workspace = true
+thiserror.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+lightning-invoice.workspace = true
+uuid = { workspace = true, optional = true }
+utoipa = { workspace = true, optional = true }
+futures.workspace = true
+serde_json.workspace = true
+serde_with.workspace = true
+tonic.workspace = true
+prost.workspace = true
+tokio-stream.workspace = true
+tokio-util = { workspace = true, default-features = false }
+
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+tokio = { workspace = true, features = [
+    "rt-multi-thread",
+    "time",
+    "macros",
+    "sync",
+    "signal"
+] }
+
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
+
+[dev-dependencies]
+rand.workspace = true
+bip39.workspace = true
+
+[build-dependencies]
+tonic-build.workspace = true

+ 77 - 0
crates/cdk-payment-processor/README.md

@@ -0,0 +1,77 @@
+# CDK Payment Processor
+
+The cdk-payment-processor is a Rust crate that provides both a binary and a library for handling payments to and from a cdk mint. 
+
+## Overview
+
+### Library Components
+- **Payment Processor Server**: Handles interaction with payment processor backend implementations
+- **Client**: Used by mintd to query the server for payment information
+- **Backend Implementations**: Supports CLN, LND, and a fake wallet (for testing)
+
+### Features
+- Modular backend system supporting multiple Lightning implementations
+- Extensible design allowing for custom backend implementations
+
+## Building from Source
+
+### Prerequisites
+1. Install Nix package manager
+2. Enter development environment:
+```sh
+nix develop
+```
+
+### Configuration
+
+The server requires different environment variables depending on your chosen Lightning Network backend.
+
+#### Core Settings
+```sh
+# Choose backend: CLN, LND, or FAKEWALLET
+export CDK_PAYMENT_PROCESSOR_LN_BACKEND="CLN"
+
+# Server configuration
+export CDK_PAYMENT_PROCESSOR_LISTEN_HOST="127.0.0.1"
+export CDK_PAYMENT_PROCESSOR_LISTEN_PORT="8090"
+```
+
+#### Backend-Specific Configuration
+
+##### Core Lightning (CLN)
+```sh
+# Path to CLN RPC socket
+export CDK_PAYMENT_PROCESSOR_CLN_RPC_PATH="/path/to/lightning-rpc"
+```
+
+##### Lightning Network Daemon (LND)
+```sh
+# LND connection details
+export CDK_PAYMENT_PROCESSOR_LND_ADDRESS="localhost:10009"
+export CDK_PAYMENT_PROCESSOR_LND_CERT_FILE="/path/to/tls.cert"
+export CDK_PAYMENT_PROCESSOR_LND_MACAROON_FILE="/path/to/macaroon"
+```
+
+### Building and Running
+
+Build and run the binary with your chosen backend:
+
+```sh
+# For CLN backend
+cargo run --bin cdk-payment-processor --no-default-features --features cln
+
+# For LND backend
+cargo run --bin cdk-payment-processor --no-default-features --features lnd
+
+# For fake wallet (testing only)
+cargo run --bin cdk-payment-processor --no-default-features --features fake
+```
+
+## Development
+
+To implement a new backend:
+1. Create a new module implementing the payment processor traits
+2. Add appropriate feature flags
+3. Update the binary to support the new backend
+
+For library usage examples and API documentation, refer to the crate documentation.

+ 5 - 0
crates/cdk-payment-processor/build.rs

@@ -0,0 +1,5 @@
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    println!("cargo:rerun-if-changed=src/proto/payment_processor.proto");
+    tonic_build::compile_protos("src/proto/payment_processor.proto")?;
+    Ok(())
+}

+ 206 - 0
crates/cdk-payment-processor/src/bin/payment_processor.rs

@@ -0,0 +1,206 @@
+#[cfg(feature = "fake")]
+use std::collections::{HashMap, HashSet};
+use std::env;
+use std::path::PathBuf;
+#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))]
+use std::sync::Arc;
+
+#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))]
+use anyhow::bail;
+#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))]
+use cdk_common::common::FeeReserve;
+#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))]
+use cdk_common::payment::{self, MintPayment};
+use cdk_common::Amount;
+#[cfg(feature = "fake")]
+use cdk_fake_wallet::FakeWallet;
+use serde::{Deserialize, Serialize};
+#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))]
+use tokio::signal;
+use tracing_subscriber::EnvFilter;
+
+pub const ENV_LN_BACKEND: &str = "CDK_PAYMENT_PROCESSOR_LN_BACKEND";
+pub const ENV_LISTEN_HOST: &str = "CDK_PAYMENT_PROCESSOR_LISTEN_HOST";
+pub const ENV_LISTEN_PORT: &str = "CDK_PAYMENT_PROCESSOR_LISTEN_PORT";
+pub const ENV_PAYMENT_PROCESSOR_TLS_DIR: &str = "CDK_PAYMENT_PROCESSOR_TLS_DIR";
+
+// CLN
+pub const ENV_CLN_RPC_PATH: &str = "CDK_PAYMENT_PROCESSOR_CLN_RPC_PATH";
+pub const ENV_CLN_BOLT12: &str = "CDK_PAYMENT_PROCESSOR_CLN_BOLT12";
+
+pub const ENV_FEE_PERCENT: &str = "CDK_PAYMENT_PROCESSOR_FEE_PERCENT";
+pub const ENV_RESERVE_FEE_MIN: &str = "CDK_PAYMENT_PROCESSOR_RESERVE_FEE_MIN";
+
+// LND environment variables
+pub const ENV_LND_ADDRESS: &str = "CDK_PAYMENT_PROCESSOR_LND_ADDRESS";
+pub const ENV_LND_CERT_FILE: &str = "CDK_PAYMENT_PROCESSOR_LND_CERT_FILE";
+pub const ENV_LND_MACAROON_FILE: &str = "CDK_PAYMENT_PROCESSOR_LND_MACAROON_FILE";
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+    let default_filter = "debug";
+
+    let sqlx_filter = "sqlx=warn";
+    let hyper_filter = "hyper=warn";
+    let h2_filter = "h2=warn";
+    let rustls_filter = "rustls=warn";
+
+    let env_filter = EnvFilter::new(format!(
+        "{},{},{},{},{}",
+        default_filter, sqlx_filter, hyper_filter, h2_filter, rustls_filter
+    ));
+
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
+
+    #[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))]
+    {
+        let ln_backend: String = env::var(ENV_LN_BACKEND)?;
+        let listen_addr: String = env::var(ENV_LISTEN_HOST)?;
+        let listen_port: u16 = env::var(ENV_LISTEN_PORT)?.parse()?;
+        let tls_dir: Option<PathBuf> = env::var(ENV_PAYMENT_PROCESSOR_TLS_DIR)
+            .ok()
+            .map(PathBuf::from);
+
+        let ln_backed: Arc<dyn MintPayment<Err = payment::Error> + Send + Sync> =
+            match ln_backend.to_uppercase().as_str() {
+                #[cfg(feature = "cln")]
+                "CLN" => {
+                    let cln_settings = Cln::default().from_env();
+                    let fee_reserve = FeeReserve {
+                        min_fee_reserve: cln_settings.reserve_fee_min,
+                        percent_fee_reserve: cln_settings.fee_percent,
+                    };
+
+                    Arc::new(cdk_cln::Cln::new(cln_settings.rpc_path, fee_reserve).await?)
+                }
+                #[cfg(feature = "fake")]
+                "FAKEWALLET" => {
+                    let fee_reserve = FeeReserve {
+                        min_fee_reserve: 1.into(),
+                        percent_fee_reserve: 0.0,
+                    };
+
+                    let fake_wallet =
+                        FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0);
+
+                    Arc::new(fake_wallet)
+                }
+                #[cfg(feature = "lnd")]
+                "LND" => {
+                    let lnd_settings = Lnd::default().from_env();
+                    let fee_reserve = FeeReserve {
+                        min_fee_reserve: lnd_settings.reserve_fee_min,
+                        percent_fee_reserve: lnd_settings.fee_percent,
+                    };
+
+                    Arc::new(
+                        cdk_lnd::Lnd::new(
+                            lnd_settings.address,
+                            lnd_settings.cert_file,
+                            lnd_settings.macaroon_file,
+                            fee_reserve,
+                        )
+                        .await?,
+                    )
+                }
+
+                _ => {
+                    bail!("Unknown payment processor");
+                }
+            };
+
+        let mut server = cdk_payment_processor::PaymentProcessorServer::new(
+            ln_backed,
+            &listen_addr,
+            listen_port,
+        )?;
+
+        server.start(tls_dir).await?;
+
+        // Wait for shutdown signal
+        signal::ctrl_c().await?;
+
+        server.stop().await?;
+    }
+    Ok(())
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Cln {
+    pub rpc_path: PathBuf,
+    #[serde(default)]
+    pub bolt12: bool,
+    pub fee_percent: f32,
+    pub reserve_fee_min: Amount,
+}
+
+impl Cln {
+    pub fn from_env(mut self) -> Self {
+        // RPC Path
+        if let Ok(path) = env::var(ENV_CLN_RPC_PATH) {
+            self.rpc_path = PathBuf::from(path);
+        }
+
+        // BOLT12 flag
+        if let Ok(bolt12_str) = env::var(ENV_CLN_BOLT12) {
+            if let Ok(bolt12) = bolt12_str.parse() {
+                self.bolt12 = bolt12;
+            }
+        }
+
+        // Fee percent
+        if let Ok(fee_str) = env::var(ENV_FEE_PERCENT) {
+            if let Ok(fee) = fee_str.parse() {
+                self.fee_percent = fee;
+            }
+        }
+
+        // Reserve fee minimum
+        if let Ok(reserve_fee_str) = env::var(ENV_RESERVE_FEE_MIN) {
+            if let Ok(reserve_fee) = reserve_fee_str.parse::<u64>() {
+                self.reserve_fee_min = reserve_fee.into();
+            }
+        }
+
+        self
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Lnd {
+    pub address: String,
+    pub cert_file: PathBuf,
+    pub macaroon_file: PathBuf,
+    pub fee_percent: f32,
+    pub reserve_fee_min: Amount,
+}
+
+impl Lnd {
+    pub fn from_env(mut self) -> Self {
+        if let Ok(address) = env::var(ENV_LND_ADDRESS) {
+            self.address = address;
+        }
+
+        if let Ok(cert_path) = env::var(ENV_LND_CERT_FILE) {
+            self.cert_file = PathBuf::from(cert_path);
+        }
+
+        if let Ok(macaroon_path) = env::var(ENV_LND_MACAROON_FILE) {
+            self.macaroon_file = PathBuf::from(macaroon_path);
+        }
+
+        if let Ok(fee_str) = env::var(ENV_FEE_PERCENT) {
+            if let Ok(fee) = fee_str.parse() {
+                self.fee_percent = fee;
+            }
+        }
+
+        if let Ok(reserve_fee_str) = env::var(ENV_RESERVE_FEE_MIN) {
+            if let Ok(reserve_fee) = reserve_fee_str.parse::<u64>() {
+                self.reserve_fee_min = reserve_fee.into();
+            }
+        }
+
+        self
+    }
+}

+ 20 - 0
crates/cdk-payment-processor/src/error.rs

@@ -0,0 +1,20 @@
+//! Errors
+
+use thiserror::Error;
+
+/// CDK Payment processor error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Invalid ID
+    #[error("Invalid id")]
+    InvalidId,
+    /// NUT00 Error
+    #[error(transparent)]
+    NUT00(#[from] cdk_common::nuts::nut00::Error),
+    /// NUT05 error
+    #[error(transparent)]
+    NUT05(#[from] cdk_common::nuts::nut05::Error),
+    /// Parse invoice error
+    #[error(transparent)]
+    Invoice(#[from] lightning_invoice::ParseOrSemanticError),
+}

+ 8 - 0
crates/cdk-payment-processor/src/lib.rs

@@ -0,0 +1,8 @@
+pub mod error;
+pub mod proto;
+
+pub use proto::cdk_payment_processor_client::CdkPaymentProcessorClient;
+pub use proto::cdk_payment_processor_server::CdkPaymentProcessorServer;
+pub use proto::{PaymentProcessorClient, PaymentProcessorServer};
+#[doc(hidden)]
+pub use tonic;

+ 299 - 0
crates/cdk-payment-processor/src/proto/client.rs

@@ -0,0 +1,299 @@
+use std::path::PathBuf;
+use std::pin::Pin;
+use std::str::FromStr;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+
+use anyhow::anyhow;
+use cdk_common::payment::{
+    CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse, MintPayment,
+    PaymentQuoteResponse,
+};
+use cdk_common::{mint, Amount, CurrencyUnit, MeltOptions, MintQuoteState};
+use futures::{Stream, StreamExt};
+use serde_json::Value;
+use tokio_util::sync::CancellationToken;
+use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity};
+use tonic::{async_trait, Request};
+use tracing::instrument;
+
+use super::cdk_payment_processor_client::CdkPaymentProcessorClient;
+use super::{
+    CheckIncomingPaymentRequest, CheckOutgoingPaymentRequest, CreatePaymentRequest,
+    MakePaymentRequest, SettingsRequest, WaitIncomingPaymentRequest,
+};
+
+/// Payment Processor
+#[derive(Clone)]
+pub struct PaymentProcessorClient {
+    inner: CdkPaymentProcessorClient<Channel>,
+    wait_incoming_payment_stream_is_active: Arc<AtomicBool>,
+    cancel_incoming_payment_listener: CancellationToken,
+}
+
+impl PaymentProcessorClient {
+    /// Payment Processor
+    pub async fn new(addr: &str, port: u16, tls_dir: Option<PathBuf>) -> anyhow::Result<Self> {
+        let addr = format!("{}:{}", addr, port);
+        let channel = if let Some(tls_dir) = tls_dir {
+            // TLS directory exists, configure TLS
+
+            // Check for ca.pem
+            let ca_pem_path = tls_dir.join("ca.pem");
+            if !ca_pem_path.exists() {
+                let err_msg = format!("CA certificate file not found: {}", ca_pem_path.display());
+                tracing::error!("{}", err_msg);
+                return Err(anyhow!(err_msg));
+            }
+
+            // Check for client.pem
+            let client_pem_path = tls_dir.join("client.pem");
+            if !client_pem_path.exists() {
+                let err_msg = format!(
+                    "Client certificate file not found: {}",
+                    client_pem_path.display()
+                );
+                tracing::error!("{}", err_msg);
+                return Err(anyhow!(err_msg));
+            }
+
+            // Check for client.key
+            let client_key_path = tls_dir.join("client.key");
+            if !client_key_path.exists() {
+                let err_msg = format!("Client key file not found: {}", client_key_path.display());
+                tracing::error!("{}", err_msg);
+                return Err(anyhow!(err_msg));
+            }
+
+            let server_root_ca_cert = std::fs::read_to_string(&ca_pem_path)?;
+            let server_root_ca_cert = Certificate::from_pem(server_root_ca_cert);
+            let client_cert = std::fs::read_to_string(&client_pem_path)?;
+            let client_key = std::fs::read_to_string(&client_key_path)?;
+            let client_identity = Identity::from_pem(client_cert, client_key);
+            let tls = ClientTlsConfig::new()
+                .ca_certificate(server_root_ca_cert)
+                .identity(client_identity);
+
+            Channel::from_shared(addr)?
+                .tls_config(tls)?
+                .connect()
+                .await?
+        } else {
+            // No TLS directory, skip TLS configuration
+            Channel::from_shared(addr)?.connect().await?
+        };
+
+        let client = CdkPaymentProcessorClient::new(channel);
+
+        Ok(Self {
+            inner: client,
+            wait_incoming_payment_stream_is_active: Arc::new(AtomicBool::new(false)),
+            cancel_incoming_payment_listener: CancellationToken::new(),
+        })
+    }
+}
+
+#[async_trait]
+impl MintPayment for PaymentProcessorClient {
+    type Err = cdk_common::payment::Error;
+
+    async fn get_settings(&self) -> Result<Value, Self::Err> {
+        let mut inner = self.inner.clone();
+        let response = inner
+            .get_settings(Request::new(SettingsRequest {}))
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not get settings: {}", err);
+                cdk_common::payment::Error::Custom(err.to_string())
+            })?;
+
+        let settings = response.into_inner();
+
+        Ok(serde_json::from_str(&settings.inner)?)
+    }
+
+    /// Create a new invoice
+    async fn create_incoming_payment_request(
+        &self,
+        amount: Amount,
+        unit: &CurrencyUnit,
+        description: String,
+        unix_expiry: Option<u64>,
+    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
+        let mut inner = self.inner.clone();
+        let response = inner
+            .create_payment(Request::new(CreatePaymentRequest {
+                amount: amount.into(),
+                unit: unit.to_string(),
+                description,
+                unix_expiry,
+            }))
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not create payment request: {}", err);
+                cdk_common::payment::Error::Custom(err.to_string())
+            })?;
+
+        let response = response.into_inner();
+
+        Ok(response.try_into().map_err(|_| {
+            cdk_common::payment::Error::Anyhow(anyhow!("Could not create create payment response"))
+        })?)
+    }
+
+    async fn get_payment_quote(
+        &self,
+        request: &str,
+        unit: &CurrencyUnit,
+        options: Option<MeltOptions>,
+    ) -> Result<PaymentQuoteResponse, Self::Err> {
+        let mut inner = self.inner.clone();
+        let response = inner
+            .get_payment_quote(Request::new(super::PaymentQuoteRequest {
+                request: request.to_string(),
+                unit: unit.to_string(),
+                options: options.map(|o| o.into()),
+            }))
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not get payment quote: {}", err);
+                cdk_common::payment::Error::Custom(err.to_string())
+            })?;
+
+        let response = response.into_inner();
+
+        Ok(response.into())
+    }
+
+    async fn make_payment(
+        &self,
+        melt_quote: mint::MeltQuote,
+        partial_amount: Option<Amount>,
+        max_fee_amount: Option<Amount>,
+    ) -> Result<CdkMakePaymentResponse, Self::Err> {
+        let mut inner = self.inner.clone();
+        let response = inner
+            .make_payment(Request::new(MakePaymentRequest {
+                melt_quote: Some(melt_quote.into()),
+                partial_amount: partial_amount.map(|a| a.into()),
+                max_fee_amount: max_fee_amount.map(|a| a.into()),
+            }))
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not pay payment request: {}", err);
+
+                if err.message().contains("already paid") {
+                    cdk_common::payment::Error::InvoiceAlreadyPaid
+                } else if err.message().contains("pending") {
+                    cdk_common::payment::Error::InvoicePaymentPending
+                } else {
+                    cdk_common::payment::Error::Custom(err.to_string())
+                }
+            })?;
+
+        let response = response.into_inner();
+
+        Ok(response.try_into().map_err(|_err| {
+            cdk_common::payment::Error::Anyhow(anyhow!("could not make payment"))
+        })?)
+    }
+
+    /// Listen for invoices to be paid to the mint
+    #[instrument(skip_all)]
+    async fn wait_any_incoming_payment(
+        &self,
+    ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
+        self.wait_incoming_payment_stream_is_active
+            .store(true, Ordering::SeqCst);
+        tracing::debug!("Client waiting for payment");
+        let mut inner = self.inner.clone();
+        let stream = inner
+            .wait_incoming_payment(WaitIncomingPaymentRequest {})
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not check incoming payment stream: {}", err);
+                cdk_common::payment::Error::Custom(err.to_string())
+            })?
+            .into_inner();
+
+        let cancel_token = self.cancel_incoming_payment_listener.clone();
+        let cancel_fut = cancel_token.cancelled_owned();
+        let active_flag = self.wait_incoming_payment_stream_is_active.clone();
+
+        let transformed_stream = stream
+            .take_until(cancel_fut)
+            .filter_map(|item| async move {
+                match item {
+                    Ok(value) => {
+                        tracing::warn!("{}", value.lookup_id);
+                        Some(value.lookup_id)
+                    }
+                    Err(e) => {
+                        tracing::error!("Error in payment stream: {}", e);
+                        None // Skip this item and continue with the stream
+                    }
+                }
+            })
+            .inspect(move |_| {
+                active_flag.store(false, Ordering::SeqCst);
+                tracing::info!("Payment stream inactive");
+            });
+
+        Ok(Box::pin(transformed_stream))
+    }
+
+    /// Is wait invoice active
+    fn is_wait_invoice_active(&self) -> bool {
+        self.wait_incoming_payment_stream_is_active
+            .load(Ordering::SeqCst)
+    }
+
+    /// Cancel wait invoice
+    fn cancel_wait_invoice(&self) {
+        self.cancel_incoming_payment_listener.cancel();
+    }
+
+    async fn check_incoming_payment_status(
+        &self,
+        request_lookup_id: &str,
+    ) -> Result<MintQuoteState, Self::Err> {
+        let mut inner = self.inner.clone();
+        let response = inner
+            .check_incoming_payment(Request::new(CheckIncomingPaymentRequest {
+                request_lookup_id: request_lookup_id.to_string(),
+            }))
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not check incoming payment: {}", err);
+                cdk_common::payment::Error::Custom(err.to_string())
+            })?;
+
+        let check_incoming = response.into_inner();
+
+        let status = check_incoming.status().as_str_name();
+
+        Ok(MintQuoteState::from_str(status)?)
+    }
+
+    async fn check_outgoing_payment(
+        &self,
+        request_lookup_id: &str,
+    ) -> Result<CdkMakePaymentResponse, Self::Err> {
+        let mut inner = self.inner.clone();
+        let response = inner
+            .check_outgoing_payment(Request::new(CheckOutgoingPaymentRequest {
+                request_lookup_id: request_lookup_id.to_string(),
+            }))
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not check outgoing payment: {}", err);
+                cdk_common::payment::Error::Custom(err.to_string())
+            })?;
+
+        let check_outgoing = response.into_inner();
+
+        Ok(check_outgoing
+            .try_into()
+            .map_err(|_| cdk_common::payment::Error::UnknownPaymentState)?)
+    }
+}

+ 207 - 0
crates/cdk-payment-processor/src/proto/mod.rs

@@ -0,0 +1,207 @@
+use std::str::FromStr;
+
+use cdk_common::payment::{
+    CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse,
+};
+use cdk_common::{Bolt11Invoice, CurrencyUnit, MeltQuoteBolt11Request};
+use melt_options::Options;
+mod client;
+mod server;
+
+pub use client::PaymentProcessorClient;
+pub use server::PaymentProcessorServer;
+
+tonic::include_proto!("cdk_payment_processor");
+
+impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
+    type Error = crate::error::Error;
+    fn try_from(value: MakePaymentResponse) -> Result<Self, Self::Error> {
+        Ok(Self {
+            payment_lookup_id: value.payment_lookup_id.clone(),
+            payment_proof: value.payment_proof.clone(),
+            status: value.status().as_str_name().parse()?,
+            total_spent: value.total_spent.into(),
+            unit: value.unit.parse()?,
+        })
+    }
+}
+
+impl From<CdkMakePaymentResponse> for MakePaymentResponse {
+    fn from(value: CdkMakePaymentResponse) -> Self {
+        Self {
+            payment_lookup_id: value.payment_lookup_id.clone(),
+            payment_proof: value.payment_proof.clone(),
+            status: QuoteState::from(value.status).into(),
+            total_spent: value.total_spent.into(),
+            unit: value.unit.to_string(),
+        }
+    }
+}
+
+impl From<CreateIncomingPaymentResponse> for CreatePaymentResponse {
+    fn from(value: CreateIncomingPaymentResponse) -> Self {
+        Self {
+            request_lookup_id: value.request_lookup_id,
+            request: value.request.to_string(),
+            expiry: value.expiry,
+        }
+    }
+}
+
+impl TryFrom<CreatePaymentResponse> for CreateIncomingPaymentResponse {
+    type Error = crate::error::Error;
+
+    fn try_from(value: CreatePaymentResponse) -> Result<Self, Self::Error> {
+        Ok(Self {
+            request_lookup_id: value.request_lookup_id,
+            request: value.request,
+            expiry: value.expiry,
+        })
+    }
+}
+
+impl From<&MeltQuoteBolt11Request> for PaymentQuoteRequest {
+    fn from(value: &MeltQuoteBolt11Request) -> Self {
+        Self {
+            request: value.request.to_string(),
+            unit: value.unit.to_string(),
+            options: value.options.map(|o| o.into()),
+        }
+    }
+}
+
+impl From<cdk_common::payment::PaymentQuoteResponse> for PaymentQuoteResponse {
+    fn from(value: cdk_common::payment::PaymentQuoteResponse) -> Self {
+        Self {
+            request_lookup_id: value.request_lookup_id,
+            amount: value.amount.into(),
+            fee: value.fee.into(),
+            state: QuoteState::from(value.state).into(),
+        }
+    }
+}
+
+impl From<cdk_common::nut05::MeltOptions> for MeltOptions {
+    fn from(value: cdk_common::nut05::MeltOptions) -> Self {
+        Self {
+            options: Some(value.into()),
+        }
+    }
+}
+
+impl From<cdk_common::nut05::MeltOptions> for Options {
+    fn from(value: cdk_common::nut05::MeltOptions) -> Self {
+        match value {
+            cdk_common::MeltOptions::Mpp { mpp } => Self::Mpp(Mpp {
+                amount: mpp.amount.into(),
+            }),
+        }
+    }
+}
+
+impl From<MeltOptions> for cdk_common::nut05::MeltOptions {
+    fn from(value: MeltOptions) -> Self {
+        let options = value.options.expect("option defined");
+        match options {
+            Options::Mpp(mpp) => cdk_common::MeltOptions::new_mpp(mpp.amount),
+        }
+    }
+}
+
+impl From<PaymentQuoteResponse> for cdk_common::payment::PaymentQuoteResponse {
+    fn from(value: PaymentQuoteResponse) -> Self {
+        Self {
+            request_lookup_id: value.request_lookup_id.clone(),
+            amount: value.amount.into(),
+            fee: value.fee.into(),
+            state: value.state().into(),
+        }
+    }
+}
+
+impl From<QuoteState> for cdk_common::nut05::QuoteState {
+    fn from(value: QuoteState) -> Self {
+        match value {
+            QuoteState::Unpaid => Self::Unpaid,
+            QuoteState::Paid => Self::Paid,
+            QuoteState::Pending => Self::Pending,
+            QuoteState::Unknown => Self::Unknown,
+            QuoteState::Failed => Self::Failed,
+            QuoteState::Issued => Self::Unknown,
+        }
+    }
+}
+
+impl From<cdk_common::nut05::QuoteState> for QuoteState {
+    fn from(value: cdk_common::nut05::QuoteState) -> Self {
+        match value {
+            cdk_common::MeltQuoteState::Unpaid => Self::Unpaid,
+            cdk_common::MeltQuoteState::Paid => Self::Paid,
+            cdk_common::MeltQuoteState::Pending => Self::Pending,
+            cdk_common::MeltQuoteState::Unknown => Self::Unknown,
+            cdk_common::MeltQuoteState::Failed => Self::Failed,
+        }
+    }
+}
+
+impl From<cdk_common::nut04::QuoteState> for QuoteState {
+    fn from(value: cdk_common::nut04::QuoteState) -> Self {
+        match value {
+            cdk_common::MintQuoteState::Unpaid => Self::Unpaid,
+            cdk_common::MintQuoteState::Paid => Self::Paid,
+            cdk_common::MintQuoteState::Pending => Self::Pending,
+            cdk_common::MintQuoteState::Issued => Self::Issued,
+        }
+    }
+}
+
+impl From<cdk_common::mint::MeltQuote> for MeltQuote {
+    fn from(value: cdk_common::mint::MeltQuote) -> Self {
+        Self {
+            id: value.id.to_string(),
+            unit: value.unit.to_string(),
+            amount: value.amount.into(),
+            request: value.request,
+            fee_reserve: value.fee_reserve.into(),
+            state: QuoteState::from(value.state).into(),
+            expiry: value.expiry,
+            payment_preimage: value.payment_preimage,
+            request_lookup_id: value.request_lookup_id,
+            msat_to_pay: value.msat_to_pay.map(|a| a.into()),
+        }
+    }
+}
+
+impl TryFrom<MeltQuote> for cdk_common::mint::MeltQuote {
+    type Error = crate::error::Error;
+
+    fn try_from(value: MeltQuote) -> Result<Self, Self::Error> {
+        Ok(Self {
+            id: value
+                .id
+                .parse()
+                .map_err(|_| crate::error::Error::InvalidId)?,
+            unit: value.unit.parse()?,
+            amount: value.amount.into(),
+            request: value.request.clone(),
+            fee_reserve: value.fee_reserve.into(),
+            state: cdk_common::nut05::QuoteState::from(value.state()),
+            expiry: value.expiry,
+            payment_preimage: value.payment_preimage,
+            request_lookup_id: value.request_lookup_id,
+            msat_to_pay: value.msat_to_pay.map(|a| a.into()),
+        })
+    }
+}
+
+impl TryFrom<PaymentQuoteRequest> for MeltQuoteBolt11Request {
+    type Error = crate::error::Error;
+
+    fn try_from(value: PaymentQuoteRequest) -> Result<Self, Self::Error> {
+        Ok(Self {
+            request: Bolt11Invoice::from_str(&value.request)?,
+            unit: CurrencyUnit::from_str(&value.unit)?,
+            options: value.options.map(|o| o.into()),
+        })
+    }
+}

+ 113 - 0
crates/cdk-payment-processor/src/proto/payment_processor.proto

@@ -0,0 +1,113 @@
+syntax = "proto3";
+
+package cdk_payment_processor;
+
+service CdkPaymentProcessor {  
+    rpc GetSettings(SettingsRequest) returns (SettingsResponse) {}
+    rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {}
+    rpc GetPaymentQuote(PaymentQuoteRequest) returns (PaymentQuoteResponse) {}
+    rpc MakePayment(MakePaymentRequest) returns (MakePaymentResponse) {}
+    rpc CheckIncomingPayment(CheckIncomingPaymentRequest) returns (CheckIncomingPaymentResponse) {}
+    rpc CheckOutgoingPayment(CheckOutgoingPaymentRequest) returns (MakePaymentResponse) {}
+    rpc WaitIncomingPayment(WaitIncomingPaymentRequest) returns (stream WaitIncomingPaymentResponse) {}
+}
+
+message SettingsRequest {}
+
+message SettingsResponse {
+  string inner = 1;
+}
+
+message CreatePaymentRequest {
+  uint64 amount = 1;
+  string unit = 2;
+  string description = 3;
+  optional uint64 unix_expiry = 4;
+}
+
+message CreatePaymentResponse {
+  string request_lookup_id = 1;
+  string request = 2;
+  optional uint64 expiry = 3;
+}
+
+message Mpp {
+    uint64 amount = 1;
+}
+
+message MeltOptions {
+    oneof options {
+        Mpp mpp = 1;
+    }
+}
+
+message PaymentQuoteRequest {
+  string request = 1;
+  string unit = 2;
+  optional MeltOptions options = 3;
+}
+
+enum QuoteState {
+    UNPAID = 0;
+    PAID = 1;
+    PENDING = 2;
+    UNKNOWN = 3;
+    FAILED = 4;
+    ISSUED = 5;
+}
+
+
+message PaymentQuoteResponse {
+  string request_lookup_id = 1;
+  uint64 amount = 2;
+  uint64 fee = 3;
+  QuoteState state = 4;
+}
+
+message MeltQuote {
+    string id = 1;
+    string unit = 2;
+    uint64 amount = 3;
+    string request = 4;
+    uint64 fee_reserve = 5;
+    QuoteState state = 6;
+    uint64 expiry = 7;
+    optional string payment_preimage = 8;
+    string request_lookup_id = 9;
+    optional uint64 msat_to_pay = 10;
+}
+
+message MakePaymentRequest {
+  MeltQuote melt_quote = 1;
+  optional uint64 partial_amount = 2;
+  optional uint64 max_fee_amount = 3;
+}
+
+message MakePaymentResponse {
+  string payment_lookup_id = 1;
+  optional string payment_proof = 2;
+  QuoteState status = 3;
+  uint64 total_spent = 4;
+  string unit = 5;
+}
+
+message CheckIncomingPaymentRequest {
+  string request_lookup_id = 1;
+}
+
+message CheckIncomingPaymentResponse {
+  QuoteState status = 1;
+}
+
+message CheckOutgoingPaymentRequest {
+  string request_lookup_id = 1;
+}
+
+
+message WaitIncomingPaymentRequest {
+}
+
+
+message WaitIncomingPaymentResponse {
+  string lookup_id = 1;
+}

+ 345 - 0
crates/cdk-payment-processor/src/proto/server.rs

@@ -0,0 +1,345 @@
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use std::pin::Pin;
+use std::str::FromStr;
+use std::sync::Arc;
+use std::time::Duration;
+
+use cdk_common::payment::MintPayment;
+use futures::{Stream, StreamExt};
+use serde_json::Value;
+use tokio::sync::{mpsc, Notify};
+use tokio::task::JoinHandle;
+use tokio::time::{sleep, Instant};
+use tokio_stream::wrappers::ReceiverStream;
+use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig};
+use tonic::{async_trait, Request, Response, Status};
+use tracing::instrument;
+
+use super::cdk_payment_processor_server::{CdkPaymentProcessor, CdkPaymentProcessorServer};
+use crate::proto::*;
+
+type ResponseStream =
+    Pin<Box<dyn Stream<Item = Result<WaitIncomingPaymentResponse, Status>> + Send>>;
+
+/// Payment Processor
+#[derive(Clone)]
+pub struct PaymentProcessorServer {
+    inner: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
+    socket_addr: SocketAddr,
+    shutdown: Arc<Notify>,
+    handle: Option<Arc<JoinHandle<anyhow::Result<()>>>>,
+}
+
+impl PaymentProcessorServer {
+    pub fn new(
+        payment_processor: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
+        addr: &str,
+        port: u16,
+    ) -> anyhow::Result<Self> {
+        let socket_addr = SocketAddr::new(addr.parse()?, port);
+        Ok(Self {
+            inner: payment_processor,
+            socket_addr,
+            shutdown: Arc::new(Notify::new()),
+            handle: None,
+        })
+    }
+
+    /// Start fake wallet grpc server
+    pub async fn start(&mut self, tls_dir: Option<PathBuf>) -> anyhow::Result<()> {
+        tracing::info!("Starting RPC server {}", self.socket_addr);
+
+        let server = match tls_dir {
+            Some(tls_dir) => {
+                tracing::info!("TLS configuration found, starting secure server");
+
+                // Check for server.pem
+                let server_pem_path = tls_dir.join("server.pem");
+                if !server_pem_path.exists() {
+                    let err_msg = format!(
+                        "TLS certificate file not found: {}",
+                        server_pem_path.display()
+                    );
+                    tracing::error!("{}", err_msg);
+                    return Err(anyhow::anyhow!(err_msg));
+                }
+
+                // Check for server.key
+                let server_key_path = tls_dir.join("server.key");
+                if !server_key_path.exists() {
+                    let err_msg = format!("TLS key file not found: {}", server_key_path.display());
+                    tracing::error!("{}", err_msg);
+                    return Err(anyhow::anyhow!(err_msg));
+                }
+
+                // Check for ca.pem
+                let ca_pem_path = tls_dir.join("ca.pem");
+                if !ca_pem_path.exists() {
+                    let err_msg =
+                        format!("CA certificate file not found: {}", ca_pem_path.display());
+                    tracing::error!("{}", err_msg);
+                    return Err(anyhow::anyhow!(err_msg));
+                }
+
+                let cert = std::fs::read_to_string(&server_pem_path)?;
+                let key = std::fs::read_to_string(&server_key_path)?;
+                let client_ca_cert = std::fs::read_to_string(&ca_pem_path)?;
+
+                let client_ca_cert = Certificate::from_pem(client_ca_cert);
+                let server_identity = Identity::from_pem(cert, key);
+                let tls_config = ServerTlsConfig::new()
+                    .identity(server_identity)
+                    .client_ca_root(client_ca_cert);
+
+                Server::builder()
+                    .tls_config(tls_config)?
+                    .add_service(CdkPaymentProcessorServer::new(self.clone()))
+            }
+            None => {
+                tracing::warn!("No valid TLS configuration found, starting insecure server");
+                Server::builder().add_service(CdkPaymentProcessorServer::new(self.clone()))
+            }
+        };
+
+        let shutdown = self.shutdown.clone();
+        let addr = self.socket_addr;
+
+        self.handle = Some(Arc::new(tokio::spawn(async move {
+            let server = server.serve_with_shutdown(addr, async {
+                shutdown.notified().await;
+            });
+
+            server.await?;
+            Ok(())
+        })));
+
+        Ok(())
+    }
+
+    /// Stop fake wallet grpc server
+    pub async fn stop(&self) -> anyhow::Result<()> {
+        const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
+
+        if let Some(handle) = &self.handle {
+            tracing::info!("Initiating server shutdown");
+            self.shutdown.notify_waiters();
+
+            let start = Instant::now();
+
+            while !handle.is_finished() {
+                if start.elapsed() >= SHUTDOWN_TIMEOUT {
+                    tracing::error!(
+                        "Server shutdown timed out after {} seconds, aborting handle",
+                        SHUTDOWN_TIMEOUT.as_secs()
+                    );
+                    handle.abort();
+                    break;
+                }
+                sleep(Duration::from_millis(100)).await;
+            }
+
+            if handle.is_finished() {
+                tracing::info!("Server shutdown completed successfully");
+            }
+        } else {
+            tracing::info!("No server handle found, nothing to stop");
+        }
+
+        Ok(())
+    }
+}
+
+impl Drop for PaymentProcessorServer {
+    fn drop(&mut self) {
+        tracing::debug!("Dropping payment process server");
+        self.shutdown.notify_one();
+    }
+}
+
+#[async_trait]
+impl CdkPaymentProcessor for PaymentProcessorServer {
+    async fn get_settings(
+        &self,
+        _request: Request<SettingsRequest>,
+    ) -> Result<Response<SettingsResponse>, Status> {
+        let settings: Value = self
+            .inner
+            .get_settings()
+            .await
+            .map_err(|_| Status::internal("Could not get settings"))?;
+
+        Ok(Response::new(SettingsResponse {
+            inner: settings.to_string(),
+        }))
+    }
+
+    async fn create_payment(
+        &self,
+        request: Request<CreatePaymentRequest>,
+    ) -> Result<Response<CreatePaymentResponse>, Status> {
+        let CreatePaymentRequest {
+            amount,
+            unit,
+            description,
+            unix_expiry,
+        } = request.into_inner();
+
+        let unit =
+            CurrencyUnit::from_str(&unit).map_err(|_| Status::invalid_argument("Invalid unit"))?;
+        let invoice_response = self
+            .inner
+            .create_incoming_payment_request(amount.into(), &unit, description, unix_expiry)
+            .await
+            .map_err(|_| Status::internal("Could not create invoice"))?;
+
+        Ok(Response::new(invoice_response.into()))
+    }
+
+    async fn get_payment_quote(
+        &self,
+        request: Request<PaymentQuoteRequest>,
+    ) -> Result<Response<PaymentQuoteResponse>, Status> {
+        let request = request.into_inner();
+
+        let options: Option<cdk_common::MeltOptions> =
+            request.options.as_ref().map(|options| (*options).into());
+
+        let payment_quote = self
+            .inner
+            .get_payment_quote(
+                &request.request,
+                &CurrencyUnit::from_str(&request.unit)
+                    .map_err(|_| Status::invalid_argument("Invalid currency unit"))?,
+                options,
+            )
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not get bolt11 melt quote: {}", err);
+                Status::internal("Could not get melt quote")
+            })?;
+
+        Ok(Response::new(payment_quote.into()))
+    }
+
+    async fn make_payment(
+        &self,
+        request: Request<MakePaymentRequest>,
+    ) -> Result<Response<MakePaymentResponse>, Status> {
+        let request = request.into_inner();
+
+        let pay_invoice = self
+            .inner
+            .make_payment(
+                request
+                    .melt_quote
+                    .ok_or(Status::invalid_argument("Meltquote is required"))?
+                    .try_into()
+                    .map_err(|_err| Status::invalid_argument("Invalid melt quote"))?,
+                request.partial_amount.map(|a| a.into()),
+                request.max_fee_amount.map(|a| a.into()),
+            )
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not make payment: {}", err);
+
+                match err {
+                    cdk_common::payment::Error::InvoiceAlreadyPaid => {
+                        Status::already_exists("Payment request already paid")
+                    }
+                    cdk_common::payment::Error::InvoicePaymentPending => {
+                        Status::already_exists("Payment request pending")
+                    }
+                    _ => Status::internal("Could not pay invoice"),
+                }
+            })?;
+
+        Ok(Response::new(pay_invoice.into()))
+    }
+
+    async fn check_incoming_payment(
+        &self,
+        request: Request<CheckIncomingPaymentRequest>,
+    ) -> Result<Response<CheckIncomingPaymentResponse>, Status> {
+        let request = request.into_inner();
+
+        let check_response = self
+            .inner
+            .check_incoming_payment_status(&request.request_lookup_id)
+            .await
+            .map_err(|_| Status::internal("Could not check incoming payment status"))?;
+
+        Ok(Response::new(CheckIncomingPaymentResponse {
+            status: QuoteState::from(check_response).into(),
+        }))
+    }
+
+    async fn check_outgoing_payment(
+        &self,
+        request: Request<CheckOutgoingPaymentRequest>,
+    ) -> Result<Response<MakePaymentResponse>, Status> {
+        let request = request.into_inner();
+
+        let check_response = self
+            .inner
+            .check_outgoing_payment(&request.request_lookup_id)
+            .await
+            .map_err(|_| Status::internal("Could not check incoming payment status"))?;
+
+        Ok(Response::new(check_response.into()))
+    }
+
+    type WaitIncomingPaymentStream = ResponseStream;
+
+    // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0)
+    #[allow(clippy::incompatible_msrv)]
+    #[instrument(skip_all)]
+    async fn wait_incoming_payment(
+        &self,
+        _request: Request<WaitIncomingPaymentRequest>,
+    ) -> Result<Response<Self::WaitIncomingPaymentStream>, Status> {
+        tracing::debug!("Server waiting for payment stream");
+        let (tx, rx) = mpsc::channel(128);
+
+        let shutdown_clone = self.shutdown.clone();
+        let ln = self.inner.clone();
+        tokio::spawn(async move {
+            loop {
+                tokio::select! {
+                _ = shutdown_clone.notified() => {
+                    tracing::info!("Shutdown signal received, stopping task for ");
+                    ln.cancel_wait_invoice();
+                    break;
+                }
+                result = ln.wait_any_incoming_payment() => {
+                    match result {
+                        Ok(mut stream) => {
+                            while let Some(request_lookup_id) = stream.next().await {
+                                                match tx.send(Result::<_, Status>::Ok(WaitIncomingPaymentResponse{lookup_id: request_lookup_id} )).await {
+                    Ok(_) => {
+                        // item (server response) was queued to be send to client
+                    }
+                    Err(item) => {
+                        tracing::error!("Error adding incoming payment to stream: {}", item);
+                        break;
+                    }
+                }
+                            }
+                        }
+                        Err(err) => {
+                            tracing::warn!("Could not get invoice stream for {}", err);
+
+                            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
+                        }
+                    }
+                }
+                }
+            }
+        });
+
+        let output_stream = ReceiverStream::new(rx);
+        Ok(Response::new(
+            Box::pin(output_stream) as Self::WaitIncomingPaymentStream
+        ))
+    }
+}

+ 3 - 3
crates/cdk-redb/src/mint/mod.rs

@@ -7,7 +7,7 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use async_trait::async_trait;
-use cdk_common::common::{LnKey, QuoteTTL};
+use cdk_common::common::{PaymentProcessorKey, QuoteTTL};
 use cdk_common::database::{self, MintDatabase};
 use cdk_common::dhke::hash_to_curve;
 use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
@@ -826,7 +826,7 @@ impl MintDatabase for MintRedbDatabase {
     async fn add_melt_request(
         &self,
         melt_request: MeltBolt11Request<Uuid>,
-        ln_key: LnKey,
+        ln_key: PaymentProcessorKey,
     ) -> Result<(), Self::Err> {
         let write_txn = self.db.begin_write().map_err(Error::from)?;
         let mut table = write_txn.open_table(MELT_REQUESTS).map_err(Error::from)?;
@@ -847,7 +847,7 @@ impl MintDatabase for MintRedbDatabase {
     async fn get_melt_request(
         &self,
         quote_id: &Uuid,
-    ) -> Result<Option<(MeltBolt11Request<Uuid>, LnKey)>, Self::Err> {
+    ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let table = read_txn.open_table(MELT_REQUESTS).map_err(Error::from)?;
 

+ 2 - 2
crates/cdk-sqlite/src/mint/memory.rs

@@ -1,7 +1,7 @@
 //! In-memory database that is provided by the `cdk-sqlite` crate, mainly for testing purposes.
 use std::collections::HashMap;
 
-use cdk_common::common::LnKey;
+use cdk_common::common::PaymentProcessorKey;
 use cdk_common::database::{self, MintDatabase};
 use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
 use cdk_common::nuts::{CurrencyUnit, Id, MeltBolt11Request, Proofs};
@@ -29,7 +29,7 @@ pub async fn new_with_state(
     melt_quotes: Vec<mint::MeltQuote>,
     pending_proofs: Proofs,
     spent_proofs: Proofs,
-    melt_request: Vec<(MeltBolt11Request<Uuid>, LnKey)>,
+    melt_request: Vec<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>,
     mint_info: MintInfo,
 ) -> Result<MintSqliteDatabase, database::Error> {
     let db = empty().await?;

+ 7 - 5
crates/cdk-sqlite/src/mint/mod.rs

@@ -6,7 +6,7 @@ use std::str::FromStr;
 
 use async_trait::async_trait;
 use bitcoin::bip32::DerivationPath;
-use cdk_common::common::{LnKey, QuoteTTL};
+use cdk_common::common::{PaymentProcessorKey, QuoteTTL};
 use cdk_common::database::{self, MintDatabase};
 use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
 use cdk_common::nut00::ProofsMethods;
@@ -1285,7 +1285,7 @@ WHERE keyset_id=?;
     async fn add_melt_request(
         &self,
         melt_request: MeltBolt11Request<Uuid>,
-        ln_key: LnKey,
+        ln_key: PaymentProcessorKey,
     ) -> Result<(), Self::Err> {
         let mut transaction = self.pool.begin().await.map_err(Error::from)?;
 
@@ -1328,7 +1328,7 @@ ON CONFLICT(id) DO UPDATE SET
     async fn get_melt_request(
         &self,
         quote_id: &Uuid,
-    ) -> Result<Option<(MeltBolt11Request<Uuid>, LnKey)>, Self::Err> {
+    ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err> {
         let mut transaction = self.pool.begin().await.map_err(Error::from)?;
 
         let rec = sqlx::query(
@@ -1708,7 +1708,9 @@ fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result<BlindSignature, Error
     })
 }
 
-fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request<Uuid>, LnKey), Error> {
+fn sqlite_row_to_melt_request(
+    row: SqliteRow,
+) -> Result<(MeltBolt11Request<Uuid>, PaymentProcessorKey), Error> {
     let quote_id: Hyphenated = row.try_get("id").map_err(Error::from)?;
     let row_inputs: String = row.try_get("inputs").map_err(Error::from)?;
     let row_outputs: Option<String> = row.try_get("outputs").map_err(Error::from)?;
@@ -1721,7 +1723,7 @@ fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request<Uuid>
         outputs: row_outputs.and_then(|o| serde_json::from_str(&o).ok()),
     };
 
-    let ln_key = LnKey {
+    let ln_key = PaymentProcessorKey {
         unit: CurrencyUnit::from_str(&row_unit)?,
         method: PaymentMethod::from_str(&row_method)?,
     };

+ 1 - 1
crates/cdk/src/lib.rs

@@ -28,7 +28,7 @@ pub use cdk_common::{
 };
 #[cfg(feature = "mint")]
 #[doc(hidden)]
-pub use cdk_common::{lightning as cdk_lightning, subscription};
+pub use cdk_common::{payment as cdk_payment, subscription};
 
 pub mod fees;
 

+ 19 - 13
crates/cdk/src/mint/builder.rs

@@ -6,18 +6,20 @@ use std::sync::Arc;
 use anyhow::anyhow;
 use bitcoin::bip32::DerivationPath;
 use cdk_common::database::{self, MintDatabase};
+use cdk_common::error::Error;
+use cdk_common::payment::Bolt11Settings;
 
 use super::nut17::SupportedMethods;
 use super::nut19::{self, CachedEndpoint};
 use super::Nuts;
 use crate::amount::Amount;
-use crate::cdk_lightning::{self, MintLightning};
+use crate::cdk_payment::{self, MintPayment};
 use crate::mint::Mint;
 use crate::nuts::{
     ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion,
     MppMethodSettings, PaymentMethod,
 };
-use crate::types::LnKey;
+use crate::types::PaymentProcessorKey;
 
 /// Cashu Mint
 #[derive(Default)]
@@ -27,7 +29,9 @@ pub struct MintBuilder {
     /// Mint Storage backend
     localstore: Option<Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>>,
     /// Ln backends for mint
-    ln: Option<HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>>,
+    ln: Option<
+        HashMap<PaymentProcessorKey, Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>>,
+    >,
     seed: Option<Vec<u8>>,
     supported_units: HashMap<CurrencyUnit, (u64, u8)>,
     custom_paths: HashMap<CurrencyUnit, DerivationPath>,
@@ -119,25 +123,27 @@ impl MintBuilder {
     }
 
     /// Add ln backend
-    pub fn add_ln_backend(
+    pub async fn add_ln_backend(
         mut self,
         unit: CurrencyUnit,
         method: PaymentMethod,
         limits: MintMeltLimits,
-        ln_backend: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
-    ) -> Self {
-        let ln_key = LnKey {
+        ln_backend: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
+    ) -> Result<Self, Error> {
+        let ln_key = PaymentProcessorKey {
             unit: unit.clone(),
-            method,
+            method: method.clone(),
         };
 
         let mut ln = self.ln.unwrap_or_default();
 
-        let settings = ln_backend.get_settings();
+        let settings = ln_backend.get_settings().await?;
+
+        let settings: Bolt11Settings = settings.try_into()?;
 
         if settings.mpp {
             let mpp_settings = MppMethodSettings {
-                method,
+                method: method.clone(),
                 unit: unit.clone(),
             };
 
@@ -150,7 +156,7 @@ impl MintBuilder {
 
         if method == PaymentMethod::Bolt11 {
             let mint_method_settings = MintMethodSettings {
-                method,
+                method: method.clone(),
                 unit: unit.clone(),
                 min_amount: Some(limits.mint_min),
                 max_amount: Some(limits.mint_max),
@@ -161,7 +167,7 @@ impl MintBuilder {
             self.mint_info.nuts.nut04.disabled = false;
 
             let melt_method_settings = MeltMethodSettings {
-                method,
+                method: method.clone(),
                 unit,
                 min_amount: Some(limits.melt_min),
                 max_amount: Some(limits.melt_max),
@@ -179,7 +185,7 @@ impl MintBuilder {
 
         self.ln = Some(ln);
 
-        self
+        Ok(self)
     }
 
     /// Set pubkey

+ 3 - 3
crates/cdk/src/mint/ln.rs

@@ -1,4 +1,4 @@
-use cdk_common::common::LnKey;
+use cdk_common::common::PaymentProcessorKey;
 use cdk_common::MintQuoteState;
 
 use super::Mint;
@@ -14,7 +14,7 @@ impl Mint {
             .await?
             .ok_or(Error::UnknownQuote)?;
 
-        let ln = match self.ln.get(&LnKey::new(
+        let ln = match self.ln.get(&PaymentProcessorKey::new(
             quote.unit.clone(),
             cdk_common::PaymentMethod::Bolt11,
         )) {
@@ -27,7 +27,7 @@ impl Mint {
         };
 
         let ln_status = ln
-            .check_incoming_invoice_status(&quote.request_lookup_id)
+            .check_incoming_payment_status(&quote.request_lookup_id)
             .await?;
 
         if ln_status != quote.state && quote.state != MintQuoteState::Issued {

+ 31 - 21
crates/cdk/src/mint/melt.rs

@@ -12,14 +12,14 @@ use super::{
     Mint, PaymentMethod, PublicKey, State,
 };
 use crate::amount::to_unit;
-use crate::cdk_lightning::{MintLightning, PayInvoiceResponse};
+use crate::cdk_payment::{MakePaymentResponse, MintPayment};
 use crate::mint::verification::Verification;
 use crate::mint::SigFlag;
 use crate::nuts::nut11::{enforce_sig_flag, EnforceSigFlag};
 use crate::nuts::MeltQuoteState;
-use crate::types::LnKey;
+use crate::types::PaymentProcessorKey;
 use crate::util::unix_time;
-use crate::{cdk_lightning, ensure_cdk, Amount, Error};
+use crate::{cdk_payment, ensure_cdk, Amount, Error};
 
 impl Mint {
     #[instrument(skip_all)]
@@ -112,22 +112,32 @@ impl Mint {
 
         let ln = self
             .ln
-            .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11))
+            .get(&PaymentProcessorKey::new(
+                unit.clone(),
+                PaymentMethod::Bolt11,
+            ))
             .ok_or_else(|| {
                 tracing::info!("Could not get ln backend for {}, bolt11 ", unit);
 
                 Error::UnsupportedUnit
             })?;
 
-        let payment_quote = ln.get_payment_quote(melt_request).await.map_err(|err| {
-            tracing::error!(
-                "Could not get payment quote for mint quote, {} bolt11, {}",
-                unit,
-                err
-            );
+        let payment_quote = ln
+            .get_payment_quote(
+                &melt_request.request.to_string(),
+                &melt_request.unit,
+                melt_request.options,
+            )
+            .await
+            .map_err(|err| {
+                tracing::error!(
+                    "Could not get payment quote for mint quote, {} bolt11, {}",
+                    unit,
+                    err
+                );
 
-            Error::UnsupportedUnit
-        })?;
+                Error::UnsupportedUnit
+            })?;
 
         // We only want to set the msats_to_pay of the melt quote if the invoice is amountless
         // or we want to ignore the amount and do an mpp payment
@@ -385,9 +395,9 @@ impl Mint {
     ) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
         use std::sync::Arc;
         async fn check_payment_state(
-            ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
+            ln: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
             melt_quote: &MeltQuote,
-        ) -> anyhow::Result<PayInvoiceResponse> {
+        ) -> anyhow::Result<MakePaymentResponse> {
             match ln
                 .check_outgoing_payment(&melt_quote.request_lookup_id)
                 .await
@@ -464,10 +474,10 @@ impl Mint {
                     _ => None,
                 };
                 tracing::debug!("partial_amount: {:?}", partial_amount);
-                let ln = match self
-                    .ln
-                    .get(&LnKey::new(quote.unit.clone(), PaymentMethod::Bolt11))
-                {
+                let ln = match self.ln.get(&PaymentProcessorKey::new(
+                    quote.unit.clone(),
+                    PaymentMethod::Bolt11,
+                )) {
                     Some(ln) => ln,
                     None => {
                         tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit);
@@ -480,7 +490,7 @@ impl Mint {
                 };
 
                 let pre = match ln
-                    .pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve))
+                    .make_payment(quote.clone(), partial_amount, Some(quote.fee_reserve))
                     .await
                 {
                     Ok(pay)
@@ -503,7 +513,7 @@ impl Mint {
                     Err(err) => {
                         // If the error is that the invoice was already paid we do not want to hold
                         // hold the proofs as pending to we reset them  and return an error.
-                        if matches!(err, cdk_lightning::Error::InvoiceAlreadyPaid) {
+                        if matches!(err, cdk_payment::Error::InvoiceAlreadyPaid) {
                             tracing::debug!("Invoice already paid, resetting melt quote");
                             if let Err(err) = self.process_unpaid_melt(melt_request).await {
                                 tracing::error!("Could not reset melt quote state: {}", err);
@@ -570,7 +580,7 @@ impl Mint {
                     }
                 }
 
-                (pre.payment_preimage, amount_spent)
+                (pre.payment_proof, amount_spent)
             }
         };
 

+ 14 - 7
crates/cdk/src/mint/mint_nut04.rs

@@ -1,3 +1,4 @@
+use cdk_common::payment::Bolt11Settings;
 use tracing::instrument;
 use uuid::Uuid;
 
@@ -7,7 +8,7 @@ use super::{
     NotificationPayload, PaymentMethod, PublicKey,
 };
 use crate::nuts::MintQuoteState;
-use crate::types::LnKey;
+use crate::types::PaymentProcessorKey;
 use crate::util::unix_time;
 use crate::{ensure_cdk, Amount, Error};
 
@@ -29,10 +30,10 @@ impl Mint {
 
         let is_above_max = settings
             .max_amount
-            .map_or(false, |max_amount| amount > max_amount);
+            .is_some_and(|max_amount| amount > max_amount);
         let is_below_min = settings
             .min_amount
-            .map_or(false, |min_amount| amount < min_amount);
+            .is_some_and(|min_amount| amount < min_amount);
         let is_out_of_range = is_above_max || is_below_min;
 
         ensure_cdk!(
@@ -64,7 +65,10 @@ impl Mint {
 
         let ln = self
             .ln
-            .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11))
+            .get(&PaymentProcessorKey::new(
+                unit.clone(),
+                PaymentMethod::Bolt11,
+            ))
             .ok_or_else(|| {
                 tracing::info!("Bolt11 mint request for unsupported unit");
 
@@ -75,17 +79,20 @@ impl Mint {
 
         let quote_expiry = unix_time() + mint_ttl;
 
-        if description.is_some() && !ln.get_settings().invoice_description {
+        let settings = ln.get_settings().await?;
+        let settings: Bolt11Settings = serde_json::from_value(settings)?;
+
+        if description.is_some() && !settings.invoice_description {
             tracing::error!("Backend does not support invoice description");
             return Err(Error::InvoiceDescriptionUnsupported);
         }
 
         let create_invoice_response = ln
-            .create_invoice(
+            .create_incoming_payment_request(
                 amount,
                 &unit,
                 description.unwrap_or("".to_string()),
-                quote_expiry,
+                Some(quote_expiry),
             )
             .await
             .map_err(|err| {

+ 19 - 18
crates/cdk/src/mint/mod.rs

@@ -5,18 +5,17 @@ use std::sync::Arc;
 
 use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
 use bitcoin::secp256k1::{self, Secp256k1};
-use cdk_common::common::{LnKey, QuoteTTL};
+use cdk_common::common::{PaymentProcessorKey, QuoteTTL};
 use cdk_common::database::{self, MintDatabase};
 use cdk_common::mint::MintKeySetInfo;
 use futures::StreamExt;
-use serde::{Deserialize, Serialize};
 use subscription::PubSubManager;
 use tokio::sync::{Notify, RwLock};
 use tokio::task::JoinSet;
 use tracing::instrument;
 use uuid::Uuid;
 
-use crate::cdk_lightning::{self, MintLightning};
+use crate::cdk_payment::{self, MintPayment};
 use crate::dhke::{sign_message, verify_message};
 use crate::error::Error;
 use crate::fees::calculate_fee;
@@ -44,7 +43,8 @@ pub struct Mint {
     /// Mint Storage backend
     pub localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
     /// Ln backends for mint
-    pub ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
+    pub ln:
+        HashMap<PaymentProcessorKey, Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>>,
     /// Subscription manager
     pub pubsub_manager: Arc<PubSubManager>,
     secp_ctx: Secp256k1<secp256k1::All>,
@@ -59,7 +59,10 @@ impl Mint {
     pub async fn new(
         seed: &[u8],
         localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
-        ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
+        ln: HashMap<
+            PaymentProcessorKey,
+            Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
+        >,
         // Hashmap where the key is the unit and value is (input fee ppk, max_order)
         supported_units: HashMap<CurrencyUnit, (u64, u8)>,
         custom_paths: HashMap<CurrencyUnit, DerivationPath>,
@@ -117,21 +120,25 @@ impl Mint {
     }
 
     /// Get mint info
+    #[instrument(skip_all)]
     pub async fn mint_info(&self) -> Result<MintInfo, Error> {
         Ok(self.localstore.get_mint_info().await?)
     }
 
     /// Set mint info
+    #[instrument(skip_all)]
     pub async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Error> {
         Ok(self.localstore.set_mint_info(mint_info).await?)
     }
 
     /// Get quote ttl
+    #[instrument(skip_all)]
     pub async fn quote_ttl(&self) -> Result<QuoteTTL, Error> {
         Ok(self.localstore.get_quote_ttl().await?)
     }
 
     /// Set quote ttl
+    #[instrument(skip_all)]
     pub async fn set_quote_ttl(&self, quote_ttl: QuoteTTL) -> Result<(), Error> {
         Ok(self.localstore.set_quote_ttl(quote_ttl).await?)
     }
@@ -139,6 +146,7 @@ impl Mint {
     /// Wait for any invoice to be paid
     /// For each backend starts a task that waits for any invoice to be paid
     /// Once invoice is paid mint quote status is updated
+    #[instrument(skip_all)]
     pub async fn wait_for_paid_invoices(&self, shutdown: Arc<Notify>) -> Result<(), Error> {
         let mint_arc = Arc::new(self.clone());
 
@@ -146,19 +154,21 @@ impl Mint {
 
         for (key, ln) in self.ln.iter() {
             if !ln.is_wait_invoice_active() {
+                tracing::info!("Wait payment for {:?} inactive starting.", key);
                 let mint = Arc::clone(&mint_arc);
                 let ln = Arc::clone(ln);
                 let shutdown = Arc::clone(&shutdown);
                 let key = key.clone();
                 join_set.spawn(async move {
             loop {
+                tracing::info!("Restarting wait for: {:?}", key);
                 tokio::select! {
                     _ = shutdown.notified() => {
                         tracing::info!("Shutdown signal received, stopping task for {:?}", key);
                         ln.cancel_wait_invoice();
                         break;
                     }
-                    result = ln.wait_any_invoice() => {
+                    result = ln.wait_any_incoming_payment() => {
                         match result {
                             Ok(mut stream) => {
                                 while let Some(request_lookup_id) = stream.next().await {
@@ -168,7 +178,7 @@ impl Mint {
                                 }
                             }
                             Err(err) => {
-                                tracing::warn!("Could not get invoice stream for {:?}: {}",key, err);
+                                tracing::warn!("Could not get incoming payment stream for {:?}: {}",key, err);
 
                                 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
                             }
@@ -432,15 +442,6 @@ impl Mint {
     }
 }
 
-/// Mint Fee Reserve
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
-pub struct FeeReserve {
-    /// Absolute expected min fee
-    pub min_fee_reserve: Amount,
-    /// Percentage expected fee
-    pub percent_fee_reserve: f32,
-}
-
 /// Generate new [`MintKeySetInfo`] from path
 #[instrument(skip_all)]
 fn create_new_keyset<C: secp256k1::Signing>(
@@ -490,7 +491,7 @@ mod tests {
     use std::str::FromStr;
 
     use bitcoin::Network;
-    use cdk_common::common::LnKey;
+    use cdk_common::common::PaymentProcessorKey;
     use cdk_sqlite::mint::memory::new_with_state;
     use secp256k1::Secp256k1;
     use uuid::Uuid;
@@ -594,7 +595,7 @@ mod tests {
         seed: &'a [u8],
         mint_info: MintInfo,
         supported_units: HashMap<CurrencyUnit, (u64, u8)>,
-        melt_requests: Vec<(MeltBolt11Request<Uuid>, LnKey)>,
+        melt_requests: Vec<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>,
     }
 
     async fn create_mint(config: MintConfig<'_>) -> Result<Mint, Error> {

+ 3 - 3
crates/cdk/src/mint/start_up_check.rs

@@ -5,7 +5,7 @@
 
 use super::{Error, Mint};
 use crate::mint::{MeltQuote, MeltQuoteState, PaymentMethod};
-use crate::types::LnKey;
+use crate::types::PaymentProcessorKey;
 
 impl Mint {
     /// Check the status of all pending mint quotes in the mint db
@@ -38,7 +38,7 @@ impl Mint {
 
             let (melt_request, ln_key) = match melt_request_ln_key {
                 None => {
-                    let ln_key = LnKey {
+                    let ln_key = PaymentProcessorKey {
                         unit: pending_quote.unit,
                         method: PaymentMethod::Bolt11,
                     };
@@ -67,7 +67,7 @@ impl Mint {
                             if let Err(err) = self
                                 .process_melt_request(
                                     &melt_request,
-                                    pay_invoice_response.payment_preimage,
+                                    pay_invoice_response.payment_proof,
                                     pay_invoice_response.total_spent,
                                 )
                                 .await

+ 1 - 1
crates/cdk/src/wallet/mod.rs

@@ -209,7 +209,7 @@ impl Wallet {
             .ok_or(Error::UnknownKeySet)?
             .input_fee_ppk;
 
-        let fee = (input_fee_ppk * count + 999) / 1000;
+        let fee = (input_fee_ppk * count).div_ceil(1000);
 
         Ok(Amount::from(fee))
     }

+ 99 - 3
justfile

@@ -1,6 +1,3 @@
-import "./misc/justfile.custom.just"
-import "./misc/test.just"
-
 alias b := build
 alias c := check
 alias t := test
@@ -66,3 +63,102 @@ typos:
 [no-exit-message]
 typos-fix:
   just typos -w
+
+itest db:
+  #!/usr/bin/env bash
+  ./misc/itests.sh "{{db}}"
+
+  
+fake-mint-itest db:
+  #!/usr/bin/env bash
+  ./misc/fake_itests.sh "{{db}}"
+
+  
+itest-payment-processor ln:
+  #!/usr/bin/env bash
+  ./misc/mintd_payment_processor.sh "{{ln}}"
+
+run-examples:
+  cargo r --example p2pk
+  cargo r --example mint-token
+  cargo r --example proof_selection
+  cargo r --example wallet
+
+check-wasm *ARGS="--target wasm32-unknown-unknown":
+  #!/usr/bin/env bash
+  set -euo pipefail
+
+  if [ ! -f Cargo.toml ]; then
+    cd {{invocation_directory()}}
+  fi
+
+  buildargs=(
+    "-p cdk"
+    "-p cdk --no-default-features"
+    "-p cdk --no-default-features --features wallet"
+    "-p cdk --no-default-features --features mint"
+  )
+
+  for arg in "${buildargs[@]}"; do
+    echo  "Checking '$arg'"
+    cargo check $arg {{ARGS}}
+    echo
+  done
+
+release m="":
+  #!/usr/bin/env bash
+  set -euo pipefail
+
+  args=(
+    "-p cashu"
+    "-p cdk-common"
+    "-p cdk"
+    "-p cdk-redb"
+    "-p cdk-sqlite"
+    "-p cdk-rexie"
+    "-p cdk-axum"
+    "-p cdk-mint-rpc"
+    "-p cdk-cln"
+    "-p cdk-lnd"
+    "-p cdk-strike"
+    "-p cdk-phoenixd"
+    "-p cdk-lnbits"
+    "-p cdk-fake-wallet"
+    "-p cdk-cli"
+    "-p cdk-mintd"
+  )
+
+  for arg in "${args[@]}";
+  do
+    echo "Publishing '$arg'"
+    cargo publish $arg {{m}}
+    echo
+  done
+
+check-docs:
+  #!/usr/bin/env bash
+  set -euo pipefail
+  args=(
+    "-p cashu"
+    "-p cdk-common"
+    "-p cdk"
+    "-p cdk-redb"
+    "-p cdk-sqlite"
+    "-p cdk-axum"
+    "-p cdk-rexie"
+    "-p cdk-cln"
+    "-p cdk-lnd"
+    "-p cdk-strike"
+    "-p cdk-phoenixd"
+    "-p cdk-lnbits"
+    "-p cdk-fake-wallet"
+    "-p cdk-mint-rpc"
+    "-p cdk-cli"
+    "-p cdk-mintd"
+  )
+
+  for arg in "${args[@]}"; do
+    echo  "Checking '$arg' docs"
+    cargo doc $arg --all-features
+    echo
+  done

+ 154 - 0
misc/mintd_payment_processor.sh

@@ -0,0 +1,154 @@
+#!/usr/bin/env bash
+
+# Function to perform cleanup
+cleanup() {
+    echo "Cleaning up..."
+
+
+    echo "Killing the cdk payment processor"
+    kill -2 $cdk_payment_processor_pid
+    wait $cdk_payment_processor_pid
+
+    echo "Killing the cdk mintd"
+    kill -2 $cdk_mintd_pid
+    wait $cdk_mintd_pid
+
+    echo "Killing the cdk regtest"
+    kill -2 $cdk_regtest_pid
+    wait $cdk_regtest_pid
+
+    echo "Mint binary terminated"
+
+    # Remove the temporary directory
+    rm -rf "$cdk_itests"
+    echo "Temp directory removed: $cdk_itests"
+    unset cdk_itests
+    unset cdk_itests_mint_addr
+    unset cdk_itests_mint_port
+}
+
+# Set up trap to call cleanup on script exit
+trap cleanup EXIT
+
+# Create a temporary directory
+export cdk_itests=$(mktemp -d)
+export cdk_itests_mint_addr="127.0.0.1";
+export cdk_itests_mint_port_0=8086;
+
+
+export LN_BACKEND="$1";
+
+URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port_0/v1/info"
+# Check if the temporary directory was created successfully
+if [[ ! -d "$cdk_itests" ]]; then
+    echo "Failed to create temp directory"
+    exit 1
+fi
+
+echo "Temp directory created: $cdk_itests"
+export MINT_DATABASE="$1";
+
+cargo build -p cdk-integration-tests 
+
+
+if [ "$LN_BACKEND" != "FAKEWALLET" ]; then
+    cargo run --bin start_regtest &
+    cdk_regtest_pid=$!
+    mkfifo "$cdk_itests/progress_pipe"
+    rm -f "$cdk_itests/signal_received"  # Ensure clean state
+    # Start reading from pipe in background
+    (while read line; do
+        case "$line" in
+            "checkpoint1")
+                echo "Reached first checkpoint"
+                touch "$cdk_itests/signal_received"
+                exit 0
+                ;;
+        esac
+    done < "$cdk_itests/progress_pipe") &
+    # Wait for up to 120 seconds
+    for ((i=0; i<120; i++)); do
+        if [ -f "$cdk_itests/signal_received" ]; then
+            echo "break signal received"
+            break
+        fi
+        sleep 1
+    done
+    echo "Regtest set up continuing"
+fi
+
+# Start payment processor
+
+
+export CDK_PAYMENT_PROCESSOR_CLN_RPC_PATH="$cdk_itests/cln/one/regtest/lightning-rpc";
+
+export CDK_PAYMENT_PROCESSOR_LND_ADDRESS="https://localhost:10010";
+export CDK_PAYMENT_PROCESSOR_LND_CERT_FILE="$cdk_itests/lnd/two/tls.cert";
+export CDK_PAYMENT_PROCESSOR_LND_MACAROON_FILE="$cdk_itests/lnd/two/data/chain/bitcoin/regtest/admin.macaroon";
+
+export CDK_PAYMENT_PROCESSOR_LN_BACKEND=$LN_BACKEND;
+export CDK_PAYMENT_PROCESSOR_LISTEN_HOST="127.0.0.1";
+export CDK_PAYMENT_PROCESSOR_LISTEN_PORT="8090";
+
+echo "$CDK_PAYMENT_PROCESSOR_CLN_RPC_PATH"
+
+cargo run --bin cdk-payment-processor &
+
+cdk_payment_processor_pid=$!
+
+sleep 10;
+
+export CDK_MINTD_URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port_0";
+export CDK_MINTD_WORK_DIR="$cdk_itests";
+export CDK_MINTD_LISTEN_HOST=$cdk_itests_mint_addr;
+export CDK_MINTD_LISTEN_PORT=$cdk_itests_mint_port_0;
+export CDK_MINTD_LN_BACKEND="grpcprocessor";
+export CDK_MINTD_GRPC_PAYMENT_PROCESSOR_ADDRESS="http://127.0.0.1";
+export CDK_MINTD_GRPC_PAYMENT_PROCESSOR_PORT="8090";
+export CDK_MINTD_GRPC_PAYMENT_PROCESSOR_SUPPORTED_UNITS="sat";
+export CDK_MINTD_MNEMONIC="eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal";
+ 
+cargo run --bin cdk-mintd --no-default-features --features grpc-processor &
+cdk_mintd_pid=$!
+
+echo $cdk_itests
+
+TIMEOUT=100
+START_TIME=$(date +%s)
+# Loop until the endpoint returns a 200 OK status or timeout is reached
+while true; do
+    # Get the current time
+    CURRENT_TIME=$(date +%s)
+    
+    # Calculate the elapsed time
+    ELAPSED_TIME=$((CURRENT_TIME - START_TIME))
+
+    # Check if the elapsed time exceeds the timeout
+    if [ $ELAPSED_TIME -ge $TIMEOUT ]; then
+        echo "Timeout of $TIMEOUT seconds reached. Exiting..."
+        exit 1
+    fi
+
+    # Make a request to the endpoint and capture the HTTP status code
+    HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" $URL)
+
+    # Check if the HTTP status is 200 OK
+    if [ "$HTTP_STATUS" -eq 200 ]; then
+        echo "Received 200 OK from $URL"
+        break
+    else
+        echo "Waiting for 200 OK response, current status: $HTTP_STATUS"
+        sleep 2  # Wait for 2 seconds before retrying
+    fi
+done
+
+
+cargo test -p cdk-integration-tests --test payment_processor
+
+# Run cargo test
+# cargo test -p cdk-integration-tests --test fake_wallet
+# Capture the exit status of cargo test
+test_status=$?
+
+# Exit with the status of the tests
+exit $test_status

+ 0 - 9
misc/test.just

@@ -1,9 +0,0 @@
-itest db:
-  #!/usr/bin/env bash
-  ./misc/itests.sh "{{db}}"
-
-  
-fake-mint-itest db:
-  #!/usr/bin/env bash
-  ./misc/fake_itests.sh "{{db}}"
-