thesimplekid 4 週間 前
コミット
ae6c107809
86 ファイル変更6260 行追加1929 行削除
  1. 1 0
      Cargo.toml
  2. 1 0
      crates/cashu/Cargo.toml
  3. 46 0
      crates/cashu/src/amount.rs
  4. 25 3
      crates/cashu/src/nuts/auth/nut21.rs
  5. 3 1
      crates/cashu/src/nuts/auth/nut22.rs
  6. 2 0
      crates/cashu/src/nuts/mod.rs
  7. 4 1
      crates/cashu/src/nuts/nut00/mod.rs
  8. 10 0
      crates/cashu/src/nuts/nut04.rs
  9. 18 1
      crates/cashu/src/nuts/nut05.rs
  10. 34 0
      crates/cashu/src/nuts/nut17/mod.rs
  11. 6 0
      crates/cashu/src/nuts/nut19.rs
  12. 0 6
      crates/cashu/src/nuts/nut23.rs
  13. 104 0
      crates/cashu/src/nuts/nut24.rs
  14. 213 0
      crates/cdk-axum/src/bolt12_router.rs
  15. 36 4
      crates/cdk-axum/src/lib.rs
  16. 7 9
      crates/cdk-axum/src/router_handlers.rs
  17. 4 0
      crates/cdk-cli/Cargo.toml
  18. 132 0
      crates/cdk-cli/src/bip353.rs
  19. 2 0
      crates/cdk-cli/src/main.rs
  20. 275 143
      crates/cdk-cli/src/sub_commands/melt.rs
  21. 101 24
      crates/cdk-cli/src/sub_commands/mint.rs
  22. 6 0
      crates/cdk-cln/src/error.rs
  23. 568 187
      crates/cdk-cln/src/lib.rs
  24. 1 0
      crates/cdk-common/Cargo.toml
  25. 24 15
      crates/cdk-common/src/database/mint/mod.rs
  26. 3 0
      crates/cdk-common/src/database/mod.rs
  27. 18 0
      crates/cdk-common/src/error.rs
  28. 2 0
      crates/cdk-common/src/lib.rs
  29. 26 0
      crates/cdk-common/src/melt.rs
  30. 283 28
      crates/cdk-common/src/mint.rs
  31. 225 18
      crates/cdk-common/src/payment.rs
  32. 3 0
      crates/cdk-common/src/subscription.rs
  33. 67 2
      crates/cdk-common/src/wallet.rs
  34. 3 0
      crates/cdk-common/src/ws.rs
  35. 2 1
      crates/cdk-fake-wallet/Cargo.toml
  36. 1 1
      crates/cdk-fake-wallet/src/error.rs
  37. 260 112
      crates/cdk-fake-wallet/src/lib.rs
  38. 57 3
      crates/cdk-integration-tests/src/init_pure_tests.rs
  39. 35 10
      crates/cdk-integration-tests/src/lib.rs
  40. 332 0
      crates/cdk-integration-tests/tests/bolt12.rs
  41. 3 3
      crates/cdk-integration-tests/tests/fake_auth.rs
  42. 10 2
      crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs
  43. 3 3
      crates/cdk-integration-tests/tests/integration_tests_pure.rs
  44. 2 2
      crates/cdk-integration-tests/tests/regtest.rs
  45. 4 0
      crates/cdk-integration-tests/tests/test_fees.rs
  46. 3 0
      crates/cdk-lnbits/src/error.rs
  47. 197 144
      crates/cdk-lnbits/src/lib.rs
  48. 293 235
      crates/cdk-lnd/src/lib.rs
  49. 1 0
      crates/cdk-mint-rpc/Cargo.toml
  50. 41 8
      crates/cdk-mint-rpc/src/proto/server.rs
  51. 56 33
      crates/cdk-mintd/src/main.rs
  52. 4 1
      crates/cdk-payment-processor/Cargo.toml
  53. 64 3
      crates/cdk-payment-processor/src/error.rs
  54. 1 0
      crates/cdk-payment-processor/src/lib.rs
  55. 115 47
      crates/cdk-payment-processor/src/proto/client.rs
  56. 172 110
      crates/cdk-payment-processor/src/proto/mod.rs
  57. 98 35
      crates/cdk-payment-processor/src/proto/payment_processor.proto
  58. 154 62
      crates/cdk-payment-processor/src/proto/server.rs
  59. 3 0
      crates/cdk-sqlite/src/mint/error.rs
  60. 1 1
      crates/cdk-sqlite/src/mint/memory.rs
  61. 1 0
      crates/cdk-sqlite/src/mint/migrations.rs
  62. 81 0
      crates/cdk-sqlite/src/mint/migrations/20250706101057_bolt12.sql
  63. 480 224
      crates/cdk-sqlite/src/mint/mod.rs
  64. 1 0
      crates/cdk-sqlite/src/wallet/migrations.rs
  65. 58 0
      crates/cdk-sqlite/src/wallet/migrations/20250707093445_bolt12.sql
  66. 30 10
      crates/cdk-sqlite/src/wallet/mod.rs
  67. 1 0
      crates/cdk/Cargo.toml
  68. 23 25
      crates/cdk/src/mint/builder.rs
  69. 0 301
      crates/cdk/src/mint/issue/issue_nut04.rs
  70. 599 1
      crates/cdk/src/mint/issue/mod.rs
  71. 33 16
      crates/cdk/src/mint/ln.rs
  72. 210 60
      crates/cdk/src/mint/melt.rs
  73. 42 17
      crates/cdk/src/mint/mod.rs
  74. 6 2
      crates/cdk/src/mint/start_up_check.rs
  75. 6 0
      crates/cdk/src/mint/subscription/on_subscription.rs
  76. 23 12
      crates/cdk/src/wallet/issue/issue_bolt11.rs
  77. 258 0
      crates/cdk/src/wallet/issue/issue_bolt12.rs
  78. 2 0
      crates/cdk/src/wallet/issue/mod.rs
  79. 1 1
      crates/cdk/src/wallet/melt/melt_bolt11.rs
  80. 89 0
      crates/cdk/src/wallet/melt/melt_bolt12.rs
  81. 2 0
      crates/cdk/src/wallet/melt/mod.rs
  82. 101 1
      crates/cdk/src/wallet/mint_connector/http_client.rs
  83. 26 0
      crates/cdk/src/wallet/mint_connector/mod.rs
  84. 1 1
      crates/cdk/src/wallet/mod.rs
  85. 7 0
      misc/itests.sh
  86. 14 0
      misc/mintd_payment_processor.sh

+ 1 - 0
Cargo.toml

@@ -61,6 +61,7 @@ ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
 cbor-diag = "0.1.12"
 futures = { version = "0.3.28", default-features = false, features = ["async-await"] }
 lightning-invoice = { version = "0.33.0", features = ["serde", "std"] }
+lightning = { version = "0.1.2", default-features = false, features = ["std"]}
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 thiserror = { version = "2" }

+ 1 - 0
crates/cashu/Cargo.toml

@@ -26,6 +26,7 @@ ciborium.workspace = true
 once_cell.workspace = true
 serde.workspace = true
 lightning-invoice.workspace = true
+lightning.workspace = true
 thiserror.workspace = true
 tracing.workspace = true
 url.workspace = true

+ 46 - 0
crates/cashu/src/amount.rs

@@ -6,6 +6,7 @@ use std::cmp::Ordering;
 use std::fmt;
 use std::str::FromStr;
 
+use lightning::offers::offer::Offer;
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
@@ -26,6 +27,12 @@ pub enum Error {
     /// Invalid amount
     #[error("Invalid Amount: {0}")]
     InvalidAmount(String),
+    /// Amount undefined
+    #[error("Amount undefined")]
+    AmountUndefined,
+    /// Utf8 parse error
+    #[error(transparent)]
+    Utf8ParseError(#[from] std::string::FromUtf8Error),
 }
 
 /// Amount can be any unit
@@ -181,6 +188,24 @@ impl Amount {
     ) -> Result<Amount, Error> {
         to_unit(self.0, current_unit, target_unit)
     }
+
+    /// Convert to i64
+    pub fn to_i64(self) -> Option<i64> {
+        if self.0 <= i64::MAX as u64 {
+            Some(self.0 as i64)
+        } else {
+            None
+        }
+    }
+
+    /// Create from i64, returning None if negative
+    pub fn from_i64(value: i64) -> Option<Self> {
+        if value >= 0 {
+            Some(Amount(value as u64))
+        } else {
+            None
+        }
+    }
 }
 
 impl Default for Amount {
@@ -273,6 +298,27 @@ impl std::ops::Div for Amount {
     }
 }
 
+/// Convert offer to amount in unit
+pub fn amount_for_offer(offer: &Offer, unit: &CurrencyUnit) -> Result<Amount, Error> {
+    let offer_amount = offer.amount().ok_or(Error::AmountUndefined)?;
+
+    let (amount, currency) = match offer_amount {
+        lightning::offers::offer::Amount::Bitcoin { amount_msats } => {
+            (amount_msats, CurrencyUnit::Msat)
+        }
+        lightning::offers::offer::Amount::Currency {
+            iso4217_code,
+            amount,
+        } => (
+            amount,
+            CurrencyUnit::from_str(&String::from_utf8(iso4217_code.to_vec())?)
+                .map_err(|_| Error::CannotConvertUnits)?,
+        ),
+    };
+
+    to_unit(amount, &currency, unit).map_err(|_err| Error::CannotConvertUnits)
+}
+
 /// Kinds of targeting that are supported
 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
 pub enum SplitTarget {

+ 25 - 3
crates/cashu/src/nuts/auth/nut21.rs

@@ -149,6 +149,18 @@ pub enum RoutePath {
     /// Mint Blind Auth
     #[serde(rename = "/v1/auth/blind/mint")]
     MintBlindAuth,
+    /// Bolt12 Mint Quote
+    #[serde(rename = "/v1/mint/quote/bolt12")]
+    MintQuoteBolt12,
+    /// Bolt12 Mint
+    #[serde(rename = "/v1/mint/bolt12")]
+    MintBolt12,
+    /// Bolt12 Melt Quote
+    #[serde(rename = "/v1/melt/quote/bolt12")]
+    MeltQuoteBolt12,
+    /// Bolt12 Quote
+    #[serde(rename = "/v1/melt/bolt12")]
+    MeltBolt12,
 }
 
 /// Returns [`RoutePath`]s that match regex
@@ -195,6 +207,8 @@ mod tests {
         assert!(paths.contains(&RoutePath::Checkstate));
         assert!(paths.contains(&RoutePath::Restore));
         assert!(paths.contains(&RoutePath::MintBlindAuth));
+        assert!(paths.contains(&RoutePath::MintQuoteBolt12));
+        assert!(paths.contains(&RoutePath::MintBolt12));
     }
 
     #[test]
@@ -203,13 +217,17 @@ mod tests {
         let paths = matching_route_paths("^/v1/mint/.*").unwrap();
 
         // Should match only mint paths
-        assert_eq!(paths.len(), 2);
+        assert_eq!(paths.len(), 4);
         assert!(paths.contains(&RoutePath::MintQuoteBolt11));
         assert!(paths.contains(&RoutePath::MintBolt11));
+        assert!(paths.contains(&RoutePath::MintQuoteBolt12));
+        assert!(paths.contains(&RoutePath::MintBolt12));
 
         // Should not match other paths
         assert!(!paths.contains(&RoutePath::MeltQuoteBolt11));
         assert!(!paths.contains(&RoutePath::MeltBolt11));
+        assert!(!paths.contains(&RoutePath::MeltQuoteBolt12));
+        assert!(!paths.contains(&RoutePath::MeltBolt12));
         assert!(!paths.contains(&RoutePath::Swap));
     }
 
@@ -219,9 +237,11 @@ mod tests {
         let paths = matching_route_paths(".*/quote/.*").unwrap();
 
         // Should match only quote paths
-        assert_eq!(paths.len(), 2);
+        assert_eq!(paths.len(), 4);
         assert!(paths.contains(&RoutePath::MintQuoteBolt11));
         assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
+        assert!(paths.contains(&RoutePath::MintQuoteBolt12));
+        assert!(paths.contains(&RoutePath::MeltQuoteBolt12));
 
         // Should not match non-quote paths
         assert!(!paths.contains(&RoutePath::MintBolt11));
@@ -336,12 +356,14 @@ mod tests {
             "https://example.com/.well-known/openid-configuration"
         );
         assert_eq!(settings.client_id, "client123");
-        assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
+        assert_eq!(settings.protected_endpoints.len(), 5); // 3 mint paths + 1 swap path
 
         let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
             ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
             ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
             ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
+            ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
+            ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt12),
         ]);
 
         let deserlized_protected = settings.protected_endpoints.into_iter().collect();

+ 3 - 1
crates/cashu/src/nuts/auth/nut22.rs

@@ -330,12 +330,14 @@ mod tests {
         let settings: Settings = serde_json::from_str(json).unwrap();
 
         assert_eq!(settings.bat_max_mint, 5);
-        assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
+        assert_eq!(settings.protected_endpoints.len(), 5); // 4 mint paths + 1 swap path
 
         let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
             ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
             ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
             ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
+            ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
+            ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt12),
         ]);
 
         let deserialized_protected = settings.protected_endpoints.into_iter().collect();

+ 2 - 0
crates/cashu/src/nuts/mod.rs

@@ -24,6 +24,7 @@ pub mod nut18;
 pub mod nut19;
 pub mod nut20;
 pub mod nut23;
+pub mod nut24;
 
 #[cfg(feature = "auth")]
 mod auth;
@@ -67,3 +68,4 @@ pub use nut23::{
     MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
     MintQuoteBolt11Response, QuoteState as MintQuoteState,
 };
+pub use nut24::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};

+ 4 - 1
crates/cashu/src/nuts/nut00/mod.rs

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

+ 10 - 0
crates/cashu/src/nuts/nut04.rs

@@ -291,6 +291,16 @@ impl Settings {
             .position(|settings| &settings.method == method && &settings.unit == unit)
             .map(|index| self.methods.remove(index))
     }
+
+    /// Supported nut04 methods
+    pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
+        self.methods.iter().map(|a| &a.method).collect()
+    }
+
+    /// Supported nut04 units
+    pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
+        self.methods.iter().map(|s| &s.unit).collect()
+    }
 }
 
 #[cfg(test)]

+ 18 - 1
crates/cashu/src/nuts/nut05.rs

@@ -105,6 +105,11 @@ impl TryFrom<MeltRequest<String>> for MeltRequest<Uuid> {
 
 // Basic implementation without trait bounds
 impl<Q> MeltRequest<Q> {
+    /// Quote Id
+    pub fn quote_id(&self) -> &Q {
+        &self.quote
+    }
+
     /// Get inputs (proofs)
     pub fn inputs(&self) -> &Proofs {
         &self.inputs
@@ -132,7 +137,7 @@ impl<Q: Serialize + DeserializeOwned> MeltRequest<Q> {
     }
 
     /// Total [`Amount`] of [`Proofs`]
-    pub fn proofs_amount(&self) -> Result<Amount, Error> {
+    pub fn inputs_amount(&self) -> Result<Amount, Error> {
         Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
             .map_err(|_| Error::AmountOverflow)
     }
@@ -355,6 +360,18 @@ pub struct Settings {
     pub disabled: bool,
 }
 
+impl Settings {
+    /// Supported nut05 methods
+    pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
+        self.methods.iter().map(|a| &a.method).collect()
+    }
+
+    /// Supported nut05 units
+    pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
+        self.methods.iter().map(|s| &s.unit).collect()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use serde_json::{from_str, json, to_string};

+ 34 - 0
crates/cashu/src/nuts/nut17/mod.rs

@@ -9,6 +9,7 @@ use super::PublicKey;
 use crate::nuts::{
     CurrencyUnit, MeltQuoteBolt11Response, MintQuoteBolt11Response, PaymentMethod, ProofState,
 };
+use crate::MintQuoteBolt12Response;
 
 pub mod ws;
 
@@ -69,6 +70,21 @@ impl SupportedMethods {
             commands,
         }
     }
+
+    /// Create [`SupportedMethods`] for Bolt12 with all supported commands
+    pub fn default_bolt12(unit: CurrencyUnit) -> Self {
+        let commands = vec![
+            WsCommand::Bolt12MintQuote,
+            WsCommand::Bolt12MeltQuote,
+            WsCommand::ProofState,
+        ];
+
+        Self {
+            method: PaymentMethod::Bolt12,
+            unit,
+            commands,
+        }
+    }
 }
 
 /// WebSocket commands supported by the Cashu mint
@@ -82,11 +98,23 @@ pub enum WsCommand {
     /// Command to request a Lightning payment for melting tokens
     #[serde(rename = "bolt11_melt_quote")]
     Bolt11MeltQuote,
+    /// Websocket support for Bolt12 Mint Quote
+    #[serde(rename = "bolt12_mint_quote")]
+    Bolt12MintQuote,
+    /// Websocket support for Bolt12 Melt Quote
+    #[serde(rename = "bolt12_melt_quote")]
+    Bolt12MeltQuote,
     /// Command to check the state of a proof
     #[serde(rename = "proof_state")]
     ProofState,
 }
 
+impl<T> From<MintQuoteBolt12Response<T>> for NotificationPayload<T> {
+    fn from(mint_quote: MintQuoteBolt12Response<T>) -> NotificationPayload<T> {
+        NotificationPayload::MintQuoteBolt12Response(mint_quote)
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(bound = "T: Serialize + DeserializeOwned")]
 #[serde(untagged)]
@@ -98,6 +126,8 @@ pub enum NotificationPayload<T> {
     MeltQuoteBolt11Response(MeltQuoteBolt11Response<T>),
     /// Mint Quote Bolt11 Response
     MintQuoteBolt11Response(MintQuoteBolt11Response<T>),
+    /// Mint Quote Bolt12 Response
+    MintQuoteBolt12Response(MintQuoteBolt12Response<T>),
 }
 
 impl<T> From<ProofState> for NotificationPayload<T> {
@@ -128,6 +158,10 @@ pub enum Notification {
     MeltQuoteBolt11(Uuid),
     /// MintQuote id is an Uuid
     MintQuoteBolt11(Uuid),
+    /// MintQuote id is an Uuid
+    MintQuoteBolt12(Uuid),
+    /// MintQuote id is an Uuid
+    MeltQuoteBolt12(Uuid),
 }
 
 /// Kind

+ 6 - 0
crates/cashu/src/nuts/nut19.rs

@@ -55,4 +55,10 @@ pub enum Path {
     /// Swap
     #[serde(rename = "/v1/swap")]
     Swap,
+    /// Bolt12 Mint
+    #[serde(rename = "/v1/mint/bolt12")]
+    MintBolt12,
+    /// Bolt12 Melt
+    #[serde(rename = "/v1/melt/bolt12")]
+    MeltBolt12,
 }

+ 0 - 6
crates/cashu/src/nuts/nut23.rs

@@ -54,10 +54,6 @@ pub enum QuoteState {
     Unpaid,
     /// Quote has been paid and wallet can mint
     Paid,
-    /// Minting is in progress
-    /// **Note:** This state is to be used internally but is not part of the
-    /// nut.
-    Pending,
     /// ecash issued for quote
     Issued,
 }
@@ -67,7 +63,6 @@ impl fmt::Display for QuoteState {
         match self {
             Self::Unpaid => write!(f, "UNPAID"),
             Self::Paid => write!(f, "PAID"),
-            Self::Pending => write!(f, "PENDING"),
             Self::Issued => write!(f, "ISSUED"),
         }
     }
@@ -78,7 +73,6 @@ impl FromStr for QuoteState {
 
     fn from_str(state: &str) -> Result<Self, Self::Err> {
         match state {
-            "PENDING" => Ok(Self::Pending),
             "PAID" => Ok(Self::Paid),
             "UNPAID" => Ok(Self::Unpaid),
             "ISSUED" => Ok(Self::Issued),

+ 104 - 0
crates/cashu/src/nuts/nut24.rs

@@ -0,0 +1,104 @@
+//! Bolt12
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+#[cfg(feature = "mint")]
+use uuid::Uuid;
+
+use super::{CurrencyUnit, MeltOptions, PublicKey};
+use crate::Amount;
+
+/// NUT18 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Unknown Quote State
+    #[error("Unknown quote state")]
+    UnknownState,
+    /// Amount overflow
+    #[error("Amount Overflow")]
+    AmountOverflow,
+    /// Publickey not defined
+    #[error("Publickey not defined")]
+    PublickeyUndefined,
+}
+
+/// Mint quote request [NUT-24]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MintQuoteBolt12Request {
+    /// Amount
+    pub amount: Option<Amount>,
+    /// Unit wallet would like to pay with
+    pub unit: CurrencyUnit,
+    /// Memo to create the invoice with
+    pub description: Option<String>,
+    /// Pubkey
+    pub pubkey: PublicKey,
+}
+
+/// Mint quote response [NUT-24]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
+pub struct MintQuoteBolt12Response<Q> {
+    /// Quote Id
+    pub quote: Q,
+    /// Payment request to fulfil
+    pub request: String,
+    /// Amount
+    pub amount: Option<Amount>,
+    /// Unit wallet would like to pay with
+    pub unit: CurrencyUnit,
+    /// Unix timestamp until the quote is valid
+    pub expiry: Option<u64>,
+    /// Pubkey
+    pub pubkey: PublicKey,
+    /// Amount that has been paid
+    pub amount_paid: Amount,
+    /// Amount that has been issued
+    pub amount_issued: Amount,
+}
+
+#[cfg(feature = "mint")]
+impl<Q: ToString> MintQuoteBolt12Response<Q> {
+    /// Convert the MintQuote with a quote type Q to a String
+    pub fn to_string_id(&self) -> MintQuoteBolt12Response<String> {
+        MintQuoteBolt12Response {
+            quote: self.quote.to_string(),
+            request: self.request.clone(),
+            amount: self.amount,
+            unit: self.unit.clone(),
+            expiry: self.expiry,
+            pubkey: self.pubkey,
+            amount_paid: self.amount_paid,
+            amount_issued: self.amount_issued,
+        }
+    }
+}
+
+#[cfg(feature = "mint")]
+impl From<MintQuoteBolt12Response<Uuid>> for MintQuoteBolt12Response<String> {
+    fn from(value: MintQuoteBolt12Response<Uuid>) -> Self {
+        Self {
+            quote: value.quote.to_string(),
+            request: value.request,
+            expiry: value.expiry,
+            amount_paid: value.amount_paid,
+            amount_issued: value.amount_issued,
+            pubkey: value.pubkey,
+            amount: value.amount,
+            unit: value.unit,
+        }
+    }
+}
+
+/// Melt quote request [NUT-18]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MeltQuoteBolt12Request {
+    /// Bolt12 invoice to be paid
+    pub request: String,
+    /// Unit wallet would like to pay with
+    pub unit: CurrencyUnit,
+    /// Payment Options
+    pub options: Option<MeltOptions>,
+}

+ 213 - 0
crates/cdk-axum/src/bolt12_router.rs

@@ -0,0 +1,213 @@
+use anyhow::Result;
+use axum::extract::{Json, Path, State};
+use axum::response::Response;
+#[cfg(feature = "swagger")]
+use cdk::error::ErrorResponse;
+#[cfg(feature = "auth")]
+use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
+use cdk::nuts::{
+    MeltQuoteBolt11Response, MeltQuoteBolt12Request, MeltRequest, MintQuoteBolt12Request,
+    MintQuoteBolt12Response, MintRequest, MintResponse,
+};
+use paste::paste;
+use tracing::instrument;
+use uuid::Uuid;
+
+#[cfg(feature = "auth")]
+use crate::auth::AuthHeader;
+use crate::{into_response, post_cache_wrapper, MintState};
+
+post_cache_wrapper!(post_mint_bolt12, MintRequest<Uuid>, MintResponse);
+post_cache_wrapper!(
+    post_melt_bolt12,
+    MeltRequest<Uuid>,
+    MeltQuoteBolt11Response<Uuid>
+);
+
+#[cfg_attr(feature = "swagger", utoipa::path(
+    get,
+    context_path = "/v1",
+    path = "/mint/quote/bolt12",
+    responses(
+        (status = 200, description = "Successful response", body = MintQuoteBolt12Response<String>, content_type = "application/json")
+    )
+))]
+/// Get mint bolt12 quote
+#[instrument(skip_all, fields(amount = ?payload.amount))]
+pub async fn post_mint_bolt12_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Json(payload): Json<MintQuoteBolt12Request>,
+) -> Result<Json<MintQuoteBolt12Response<Uuid>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt12),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    let quote = state
+        .mint
+        .get_mint_quote(payload.into())
+        .await
+        .map_err(into_response)?;
+
+    Ok(Json(quote.try_into().map_err(into_response)?))
+}
+
+#[cfg_attr(feature = "swagger", utoipa::path(
+    get,
+    context_path = "/v1",
+    path = "/mint/quote/bolt12/{quote_id}",
+    params(
+        ("quote_id" = String, description = "The quote ID"),
+    ),
+    responses(
+        (status = 200, description = "Successful response", body = MintQuoteBolt12Response<String>, content_type = "application/json"),
+        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
+    )
+))]
+/// Get mint bolt12 quote
+#[instrument(skip_all, fields(quote_id = ?quote_id))]
+pub async fn get_check_mint_bolt12_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Path(quote_id): Path<Uuid>,
+) -> Result<Json<MintQuoteBolt12Response<Uuid>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    let quote = state
+        .mint
+        .check_mint_quote(&quote_id)
+        .await
+        .map_err(into_response)?;
+
+    Ok(Json(quote.try_into().map_err(into_response)?))
+}
+
+#[cfg_attr(feature = "swagger", utoipa::path(
+    post,
+    context_path = "/v1",
+    path = "/mint/bolt12",
+    request_body(content = MintRequest<String>, description = "Request params", content_type = "application/json"),
+    responses(
+        (status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
+        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
+    )
+))]
+/// Request a quote for melting tokens
+#[instrument(skip_all, fields(quote_id = ?payload.quote))]
+pub async fn post_mint_bolt12(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Json(payload): Json<MintRequest<Uuid>>,
+) -> Result<Json<MintResponse>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt12),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    let res = state
+        .mint
+        .process_mint_request(payload)
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not process mint: {}", err);
+            into_response(err)
+        })?;
+
+    Ok(Json(res))
+}
+
+#[cfg_attr(feature = "swagger", utoipa::path(
+    post,
+    context_path = "/v1",
+    path = "/melt/quote/bolt12",
+    request_body(content = MeltQuoteBolt12Request, description = "Quote params", content_type = "application/json"),
+    responses(
+        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
+        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
+    )
+))]
+pub async fn post_melt_bolt12_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Json(payload): Json<MeltQuoteBolt12Request>,
+) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt12),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    let quote = state
+        .mint
+        .get_melt_quote(payload.into())
+        .await
+        .map_err(into_response)?;
+
+    Ok(Json(quote))
+}
+
+#[cfg_attr(feature = "swagger", utoipa::path(
+    post,
+    context_path = "/v1",
+    path = "/melt/bolt12",
+    request_body(content = MeltRequest<String>, description = "Melt params", content_type = "application/json"),
+    responses(
+        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
+        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
+    )
+))]
+/// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange
+///
+/// Requests tokens to be destroyed and sent out via Lightning.
+pub async fn post_melt_bolt12(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Json(payload): Json<MeltRequest<Uuid>>,
+) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt12),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    let res = state.mint.melt(&payload).await.map_err(into_response)?;
+
+    Ok(Json(res))
+}

+ 36 - 4
crates/cdk-axum/src/lib.rs

@@ -19,6 +19,7 @@ use router_handlers::*;
 
 #[cfg(feature = "auth")]
 mod auth;
+mod bolt12_router;
 pub mod cache;
 mod router_handlers;
 mod ws;
@@ -52,6 +53,11 @@ mod swagger_imports {
 #[cfg(feature = "swagger")]
 use swagger_imports::*;
 
+use crate::bolt12_router::{
+    cache_post_melt_bolt12, cache_post_mint_bolt12, get_check_mint_bolt12_quote,
+    post_melt_bolt12_quote, post_mint_bolt12_quote,
+};
+
 /// CDK Mint State
 #[derive(Clone)]
 pub struct MintState {
@@ -134,8 +140,8 @@ pub struct MintState {
 pub struct ApiDocV1;
 
 /// Create mint [`Router`] with required endpoints for cashu mint with the default cache
-pub async fn create_mint_router(mint: Arc<Mint>) -> Result<Router> {
-    create_mint_router_with_custom_cache(mint, Default::default()).await
+pub async fn create_mint_router(mint: Arc<Mint>, include_bolt12: bool) -> Result<Router> {
+    create_mint_router_with_custom_cache(mint, Default::default(), include_bolt12).await
 }
 
 async fn cors_middleware(
@@ -187,6 +193,7 @@ async fn cors_middleware(
 pub async fn create_mint_router_with_custom_cache(
     mint: Arc<Mint>,
     cache: HttpCache,
+    include_bolt12: bool,
 ) -> Result<Router> {
     let state = MintState {
         mint,
@@ -223,9 +230,34 @@ pub async fn create_mint_router_with_custom_cache(
         mint_router.nest("/v1", auth_router)
     };
 
-    let mint_router = mint_router.layer(from_fn(cors_middleware));
+    // Conditionally create and merge bolt12_router
+    let mint_router = if include_bolt12 {
+        let bolt12_router = create_bolt12_router(state.clone());
+        mint_router.nest("/v1", bolt12_router)
+    } else {
+        mint_router
+    };
 
-    let mint_router = mint_router.with_state(state);
+    let mint_router = mint_router
+        .layer(from_fn(cors_middleware))
+        .with_state(state);
 
     Ok(mint_router)
 }
+
+fn create_bolt12_router(state: MintState) -> Router<MintState> {
+    Router::new()
+        .route("/melt/quote/bolt12", post(post_melt_bolt12_quote))
+        .route(
+            "/melt/quote/bolt12/{quote_id}",
+            get(get_check_melt_bolt11_quote),
+        )
+        .route("/melt/bolt12", post(cache_post_melt_bolt12))
+        .route("/mint/quote/bolt12", post(post_mint_bolt12_quote))
+        .route(
+            "/mint/quote/bolt12/{quote_id}",
+            get(get_check_mint_bolt12_quote),
+        )
+        .route("/mint/bolt12", post(cache_post_mint_bolt12))
+        .with_state(state)
+}

+ 7 - 9
crates/cdk-axum/src/router_handlers.rs

@@ -22,6 +22,8 @@ use crate::auth::AuthHeader;
 use crate::ws::main_websocket;
 use crate::MintState;
 
+/// Macro to add cache to endpoint
+#[macro_export]
 macro_rules! post_cache_wrapper {
     ($handler:ident, $request_type:ty, $response_type:ty) => {
         paste! {
@@ -163,11 +165,11 @@ pub(crate) async fn post_mint_bolt11_quote(
 
     let quote = state
         .mint
-        .get_mint_bolt11_quote(payload)
+        .get_mint_quote(payload.into())
         .await
         .map_err(into_response)?;
 
-    Ok(Json(quote))
+    Ok(Json(quote.try_into().map_err(into_response)?))
 }
 
 #[cfg_attr(feature = "swagger", utoipa::path(
@@ -212,7 +214,7 @@ pub(crate) async fn get_check_mint_bolt11_quote(
             into_response(err)
         })?;
 
-    Ok(Json(quote))
+    Ok(Json(quote.try_into().map_err(into_response)?))
 }
 
 #[instrument(skip_all)]
@@ -299,7 +301,7 @@ pub(crate) async fn post_melt_bolt11_quote(
 
     let quote = state
         .mint
-        .get_melt_bolt11_quote(&payload)
+        .get_melt_quote(payload.into())
         .await
         .map_err(into_response)?;
 
@@ -382,11 +384,7 @@ pub(crate) async fn post_melt_bolt11(
             .map_err(into_response)?;
     }
 
-    let res = state
-        .mint
-        .melt_bolt11(&payload)
-        .await
-        .map_err(into_response)?;
+    let res = state.mint.melt(&payload).await.map_err(into_response)?;
 
     Ok(Json(res))
 }

+ 4 - 0
crates/cdk-cli/Cargo.toml

@@ -11,6 +11,8 @@ rust-version.workspace = true
 readme = "README.md"
 
 [features]
+default = ["bip353"]
+bip353 = ["dep:trust-dns-resolver"]
 sqlcipher = ["cdk-sqlite/sqlcipher"]
 # MSRV is not tracked with redb enabled
 redb = ["dep:cdk-redb"]
@@ -37,3 +39,5 @@ nostr-sdk = { version = "0.41.0", default-features = false, features = [
 reqwest.workspace = true
 url.workspace = true
 serde_with.workspace = true
+lightning.workspace = true
+trust-dns-resolver = { version = "0.23.2", optional = true }

+ 132 - 0
crates/cdk-cli/src/bip353.rs

@@ -0,0 +1,132 @@
+use std::collections::HashMap;
+use std::str::FromStr;
+
+use anyhow::{bail, Result};
+use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
+use trust_dns_resolver::TokioAsyncResolver;
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Bip353Address {
+    pub user: String,
+    pub domain: String,
+}
+
+impl Bip353Address {
+    /// Resolve a human-readable Bitcoin address
+    pub async fn resolve(self) -> Result<PaymentInstruction> {
+        // Construct DNS name
+        let dns_name = format!("{}.user._bitcoin-payment.{}", self.user, self.domain);
+
+        // Create a new resolver with DNSSEC validation
+        let mut opts = ResolverOpts::default();
+        opts.validate = true; // Enable DNSSEC validation
+
+        let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), opts);
+
+        // Query TXT records - with opts.validate=true, this will fail if DNSSEC validation fails
+        let response = resolver.txt_lookup(&dns_name).await?;
+
+        // Extract and concatenate TXT record strings
+        let mut bitcoin_uris = Vec::new();
+
+        for txt in response.iter() {
+            let txt_data: Vec<String> = txt
+                .txt_data()
+                .iter()
+                .map(|bytes| String::from_utf8_lossy(bytes).into_owned())
+                .collect();
+
+            let concatenated = txt_data.join("");
+
+            if concatenated.to_lowercase().starts_with("bitcoin:") {
+                bitcoin_uris.push(concatenated);
+            }
+        }
+
+        // BIP-353 requires exactly one Bitcoin URI
+        match bitcoin_uris.len() {
+            0 => bail!("No Bitcoin URI found"),
+            1 => PaymentInstruction::from_uri(&bitcoin_uris[0]),
+            _ => bail!("Multiple Bitcoin URIs found"),
+        }
+    }
+}
+
+impl FromStr for Bip353Address {
+    type Err = anyhow::Error;
+
+    /// Parse a human-readable Bitcoin address
+    fn from_str(address: &str) -> Result<Self, Self::Err> {
+        let addr = address.trim();
+
+        // Remove Bitcoin prefix if present
+        let addr = addr.strip_prefix("₿").unwrap_or(addr);
+
+        // Split by @
+        let parts: Vec<&str> = addr.split('@').collect();
+        if parts.len() != 2 {
+            bail!("Address is not formatted correctly")
+        }
+
+        let user = parts[0].trim();
+        let domain = parts[1].trim();
+
+        if user.is_empty() || domain.is_empty() {
+            bail!("User name and domain must not be empty")
+        }
+
+        Ok(Self {
+            user: user.to_string(),
+            domain: domain.to_string(),
+        })
+    }
+}
+
+/// Payment instruction type
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum PaymentType {
+    OnChain,
+    LightningOffer,
+}
+
+/// BIP-353 payment instruction
+#[derive(Debug, Clone)]
+pub struct PaymentInstruction {
+    pub parameters: HashMap<PaymentType, String>,
+}
+
+impl PaymentInstruction {
+    /// Parse a payment instruction from a Bitcoin URI
+    pub fn from_uri(uri: &str) -> Result<Self> {
+        if !uri.to_lowercase().starts_with("bitcoin:") {
+            bail!("URI must start with 'bitcoin:'")
+        }
+
+        let mut parameters = HashMap::new();
+
+        // Parse URI parameters
+        if let Some(query_start) = uri.find('?') {
+            let query = &uri[query_start + 1..];
+            for pair in query.split('&') {
+                if let Some(eq_pos) = pair.find('=') {
+                    let key = pair[..eq_pos].to_string();
+                    let value = pair[eq_pos + 1..].to_string();
+                    let payment_type;
+                    // Determine payment type
+                    if key.contains("lno") {
+                        payment_type = PaymentType::LightningOffer;
+                    } else if !uri[8..].contains('?') && uri.len() > 8 {
+                        // Simple on-chain address
+                        payment_type = PaymentType::OnChain;
+                    } else {
+                        continue;
+                    }
+
+                    parameters.insert(payment_type, value);
+                }
+            }
+        }
+
+        Ok(PaymentInstruction { parameters })
+    }
+}

+ 2 - 0
crates/cdk-cli/src/main.rs

@@ -18,6 +18,8 @@ use tracing::Level;
 use tracing_subscriber::EnvFilter;
 use url::Url;
 
+#[cfg(feature = "bip353")]
+mod bip353;
 mod nostr_storage;
 mod sub_commands;
 mod token_storage;

+ 275 - 143
crates/cdk-cli/src/sub_commands/melt.rs

@@ -1,20 +1,34 @@
 use std::str::FromStr;
 
-use anyhow::{bail, Result};
-use cdk::amount::MSAT_IN_SAT;
+use anyhow::{anyhow, bail, Result};
+use cdk::amount::{amount_for_offer, Amount, MSAT_IN_SAT};
+use cdk::mint_url::MintUrl;
 use cdk::nuts::{CurrencyUnit, MeltOptions};
 use cdk::wallet::multi_mint_wallet::MultiMintWallet;
 use cdk::wallet::types::WalletKey;
+use cdk::wallet::{MeltQuote, Wallet};
 use cdk::Bolt11Invoice;
-use clap::Args;
+use clap::{Args, ValueEnum};
+use lightning::offers::offer::Offer;
 use tokio::task::JoinSet;
 
+use crate::bip353::{Bip353Address, PaymentType as Bip353PaymentType};
 use crate::sub_commands::balance::mint_balances;
 use crate::utils::{
     get_number_input, get_user_input, get_wallet_by_index, get_wallet_by_mint_url,
     validate_mint_number,
 };
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+pub enum PaymentType {
+    /// BOLT11 invoice
+    Bolt11,
+    /// BOLT12 offer
+    Bolt12,
+    /// Bip353
+    Bip353,
+}
+
 #[derive(Args)]
 pub struct MeltSubCommand {
     /// Currency unit e.g. sat
@@ -26,132 +40,90 @@ pub struct MeltSubCommand {
     /// Mint URL to use for melting
     #[arg(long, conflicts_with = "mpp")]
     mint_url: Option<String>,
+    /// Payment method (bolt11 or bolt12)
+    #[arg(long, default_value = "bolt11")]
+    method: PaymentType,
 }
 
-pub async fn pay(
-    multi_mint_wallet: &MultiMintWallet,
-    sub_command_args: &MeltSubCommand,
-) -> Result<()> {
-    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
-    let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
+/// Helper function to process a melt quote and execute the payment
+async fn process_payment(wallet: &Wallet, quote: MeltQuote) -> Result<()> {
+    // Display quote information
+    println!("Quote ID: {}", quote.id);
+    println!("Amount: {}", quote.amount);
+    println!("Fee Reserve: {}", quote.fee_reserve);
+    println!("State: {}", quote.state);
+    println!("Expiry: {}", quote.expiry);
+
+    // Execute the payment
+    let melt = wallet.melt(&quote.id).await?;
+    println!("Paid: {}", melt.state);
+
+    if let Some(preimage) = melt.preimage {
+        println!("Payment preimage: {preimage}");
+    }
 
-    let mut mints = vec![];
-    let mut mint_amounts = vec![];
-    if sub_command_args.mpp {
-        // MPP functionality expects multiple mints, so mint_url flag doesn't fully apply here,
-        // but we can offer to use the specified mint as the first one if provided
-        if let Some(mint_url) = &sub_command_args.mint_url {
-            println!("Using mint URL {mint_url} as the first mint for MPP payment.");
-
-            // Check if the mint exists
-            if let Ok(_wallet) =
-                get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit.clone()).await
-            {
-                // Find the index of this mint in the mints_amounts list
-                if let Some(mint_index) = mints_amounts
-                    .iter()
-                    .position(|(url, _)| url.to_string() == *mint_url)
-                {
-                    mints.push(mint_index);
-                    let melt_amount: u64 =
-                        get_number_input("Enter amount to mint from this mint in sats.")?;
-                    mint_amounts.push(melt_amount);
-                } else {
-                    println!("Warning: Mint URL exists but no balance found. Continuing with manual selection.");
-                }
-            } else {
-                println!("Warning: Could not find wallet for the specified mint URL. Continuing with manual selection.");
-            }
-        }
-        loop {
-            let mint_number: String =
-                get_user_input("Enter mint number to melt from and -1 when done.")?;
+    Ok(())
+}
 
-            if mint_number == "-1" || mint_number.is_empty() {
-                break;
+/// Helper function to check if there are enough funds and create appropriate MeltOptions
+fn create_melt_options(
+    available_funds: u64,
+    payment_amount: Option<u64>,
+    prompt: &str,
+) -> Result<Option<MeltOptions>> {
+    match payment_amount {
+        Some(amount) => {
+            // Payment has a specified amount
+            if amount > available_funds {
+                bail!("Not enough funds; payment requires {} msats", amount);
             }
-
-            let mint_number: usize = mint_number.parse()?;
-            validate_mint_number(mint_number, mints_amounts.len())?;
-
-            mints.push(mint_number);
-            let melt_amount: u64 =
-                get_number_input("Enter amount to mint from this mint in sats.")?;
-            mint_amounts.push(melt_amount);
+            Ok(None) // Use default options
         }
+        None => {
+            // Payment doesn't have an amount, ask user for it
+            let user_amount = get_number_input::<u64>(prompt)? * MSAT_IN_SAT;
 
-        let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
-
-        let mut quotes = JoinSet::new();
-
-        for (mint, amount) in mints.iter().zip(mint_amounts) {
-            let wallet = mints_amounts[*mint].0.clone();
-
-            let wallet = multi_mint_wallet
-                .get_wallet(&WalletKey::new(wallet, unit.clone()))
-                .await
-                .expect("Known wallet");
-            let options = MeltOptions::new_mpp(amount * 1000);
-
-            let bolt11_clone = bolt11.clone();
-
-            quotes.spawn(async move {
-                let quote = wallet
-                    .melt_quote(bolt11_clone.to_string(), Some(options))
-                    .await;
+            if user_amount > available_funds {
+                bail!("Not enough funds");
+            }
 
-                (wallet, quote)
-            });
+            Ok(Some(MeltOptions::new_amountless(user_amount)))
         }
+    }
+}
 
-        let quotes = quotes.join_all().await;
+pub async fn pay(
+    multi_mint_wallet: &MultiMintWallet,
+    sub_command_args: &MeltSubCommand,
+) -> Result<()> {
+    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
+    let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
 
-        for (wallet, quote) in quotes.iter() {
-            if let Err(quote) = quote {
-                tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, quote);
-                bail!("Could not get melt quote for {}", wallet.mint_url);
-            } else {
-                let quote = quote.as_ref().unwrap();
-                println!(
-                    "Melt quote {} for mint {} of amount {} with fee {}.",
-                    quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
-                );
-            }
+    if sub_command_args.mpp {
+        // MPP logic only works with BOLT11 currently
+        if !matches!(sub_command_args.method, PaymentType::Bolt11) {
+            bail!("MPP is only supported for BOLT11 invoices");
         }
 
-        let mut melts = JoinSet::new();
+        // Collect mint numbers and amounts for MPP
+        let (mints, mint_amounts) = collect_mpp_inputs(&mints_amounts, &sub_command_args.mint_url)?;
 
-        for (wallet, quote) in quotes {
-            let quote = quote.expect("Errors checked above");
-
-            melts.spawn(async move {
-                let melt = wallet.melt(&quote.id).await;
-                (wallet, melt)
-            });
-        }
-
-        let melts = melts.join_all().await;
-
-        let mut error = false;
-
-        for (wallet, melt) in melts {
-            match melt {
-                Ok(melt) => {
-                    println!(
-                        "Melt for {} paid {} with fee of {} ",
-                        wallet.mint_url, melt.amount, melt.fee_paid
-                    );
-                }
-                Err(err) => {
-                    println!("Melt for {} failed with {}", wallet.mint_url, err);
-                    error = true;
-                }
-            }
-        }
+        // Process BOLT11 MPP payment
+        let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
 
-        if error {
-            bail!("Could not complete all melts");
-        }
+        // Get quotes from all mints
+        let quotes = get_mpp_quotes(
+            multi_mint_wallet,
+            &mints_amounts,
+            &mints,
+            &mint_amounts,
+            &unit,
+            &bolt11,
+        )
+        .await?;
+
+        // Execute all melts
+        execute_mpp_melts(quotes).await?;
     } else {
         // Get wallet either by mint URL or by index
         let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
@@ -174,47 +146,207 @@ pub async fn pay(
 
         let available_funds = <cdk::Amount as Into<u64>>::into(mint_amount) * MSAT_IN_SAT;
 
-        let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
+        // Process payment based on payment method
+        match sub_command_args.method {
+            PaymentType::Bolt11 => {
+                // Process BOLT11 payment
+                let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice")?)?;
+
+                // Determine payment amount and options
+                let prompt =
+                    "Enter the amount you would like to pay in sats for this amountless invoice.";
+                let options =
+                    create_melt_options(available_funds, bolt11.amount_milli_satoshis(), prompt)?;
+
+                // Process payment
+                let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
+                process_payment(&wallet, quote).await?;
+            }
+            PaymentType::Bolt12 => {
+                // Process BOLT12 payment (offer)
+                let offer_str = get_user_input("Enter BOLT12 offer")?;
+                let offer = Offer::from_str(&offer_str)
+                    .map_err(|e| anyhow::anyhow!("Invalid BOLT12 offer: {:?}", e))?;
+
+                // Determine if offer has an amount
+                let prompt =
+                    "Enter the amount you would like to pay in sats for this amountless offer:";
+                let amount_msat = match amount_for_offer(&offer, &CurrencyUnit::Msat) {
+                    Ok(amount) => Some(u64::from(amount)),
+                    Err(_) => None,
+                };
+
+                let options = create_melt_options(available_funds, amount_msat, prompt)?;
+
+                // Get melt quote for BOLT12
+                let quote = wallet.melt_bolt12_quote(offer_str, options).await?;
+                process_payment(&wallet, quote).await?;
+            }
+            PaymentType::Bip353 => {
+                let bip353_addr = get_user_input("Enter Bip353 address.")?;
+                let bip353_addr = Bip353Address::from_str(&bip353_addr)?;
 
-        // Determine payment amount and options
-        let options = if bolt11.amount_milli_satoshis().is_none() {
-            // Get user input for amount
-            let prompt = format!(
-                "Enter the amount you would like to pay in sats for a {} payment.",
-                if sub_command_args.mpp {
-                    "MPP"
-                } else {
-                    "amountless invoice"
-                }
-            );
+                let payment_instructions = bip353_addr.resolve().await?;
 
-            let user_amount = get_number_input::<u64>(&prompt)? * MSAT_IN_SAT;
+                let offer = payment_instructions
+                    .parameters
+                    .get(&Bip353PaymentType::LightningOffer)
+                    .ok_or(anyhow!("Offer not defined"))?;
 
-            if user_amount > available_funds {
-                bail!("Not enough funds");
+                let prompt =
+                    "Enter the amount you would like to pay in sats for this amountless offer:";
+                // BIP353 payments are always amountless for now
+                let options = create_melt_options(available_funds, None, prompt)?;
+
+                // Get melt quote for BOLT12
+                let quote = wallet.melt_bolt12_quote(offer.to_string(), options).await?;
+                process_payment(&wallet, quote).await?;
             }
+        }
+    }
+
+    Ok(())
+}
+
+/// Collect mint numbers and amounts for MPP payments
+fn collect_mpp_inputs(
+    mints_amounts: &[(MintUrl, Amount)],
+    mint_url_opt: &Option<String>,
+) -> Result<(Vec<usize>, Vec<u64>)> {
+    let mut mints = Vec::new();
+    let mut mint_amounts = Vec::new();
 
-            Some(MeltOptions::new_amountless(user_amount))
+    // If a specific mint URL was provided, try to use it as the first mint
+    if let Some(mint_url) = mint_url_opt {
+        println!("Using mint URL {mint_url} as the first mint for MPP payment.");
+
+        // Find the index of this mint in the mints_amounts list
+        if let Some(mint_index) = mints_amounts
+            .iter()
+            .position(|(url, _)| url.to_string() == *mint_url)
+        {
+            mints.push(mint_index);
+            let melt_amount: u64 =
+                get_number_input("Enter amount to mint from this mint in sats.")?;
+            mint_amounts.push(melt_amount);
         } else {
-            // Check if invoice amount exceeds available funds
-            let invoice_amount = bolt11.amount_milli_satoshis().unwrap();
-            if invoice_amount > available_funds {
-                bail!("Not enough funds");
+            println!(
+                "Warning: Mint URL not found or no balance. Continuing with manual selection."
+            );
+        }
+    }
+
+    // Continue with regular mint selection
+    loop {
+        let mint_number: String =
+            get_user_input("Enter mint number to melt from and -1 when done.")?;
+
+        if mint_number == "-1" || mint_number.is_empty() {
+            break;
+        }
+
+        let mint_number: usize = mint_number.parse()?;
+        validate_mint_number(mint_number, mints_amounts.len())?;
+
+        mints.push(mint_number);
+        let melt_amount: u64 = get_number_input("Enter amount to mint from this mint in sats.")?;
+        mint_amounts.push(melt_amount);
+    }
+
+    if mints.is_empty() {
+        bail!("No mints selected for MPP payment");
+    }
+
+    Ok((mints, mint_amounts))
+}
+
+/// Get quotes from all mints for MPP payment
+async fn get_mpp_quotes(
+    multi_mint_wallet: &MultiMintWallet,
+    mints_amounts: &[(MintUrl, Amount)],
+    mints: &[usize],
+    mint_amounts: &[u64],
+    unit: &CurrencyUnit,
+    bolt11: &Bolt11Invoice,
+) -> Result<Vec<(Wallet, MeltQuote)>> {
+    let mut quotes = JoinSet::new();
+
+    for (mint, amount) in mints.iter().zip(mint_amounts) {
+        let wallet = mints_amounts[*mint].0.clone();
+
+        let wallet = multi_mint_wallet
+            .get_wallet(&WalletKey::new(wallet, unit.clone()))
+            .await
+            .expect("Known wallet");
+        let options = MeltOptions::new_mpp(*amount * 1000);
+
+        let bolt11_clone = bolt11.clone();
+
+        quotes.spawn(async move {
+            let quote = wallet
+                .melt_quote(bolt11_clone.to_string(), Some(options))
+                .await;
+
+            (wallet, quote)
+        });
+    }
+
+    let quotes_results = quotes.join_all().await;
+
+    // Validate all quotes succeeded
+    let mut valid_quotes = Vec::new();
+    for (wallet, quote_result) in quotes_results {
+        match quote_result {
+            Ok(quote) => {
+                println!(
+                    "Melt quote {} for mint {} of amount {} with fee {}.",
+                    quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
+                );
+                valid_quotes.push((wallet, quote));
             }
-            None
-        };
+            Err(err) => {
+                tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, err);
+                bail!("Could not get melt quote for {}", wallet.mint_url);
+            }
+        }
+    }
+
+    Ok(valid_quotes)
+}
+
+/// Execute all melts for MPP payment
+async fn execute_mpp_melts(quotes: Vec<(Wallet, MeltQuote)>) -> Result<()> {
+    let mut melts = JoinSet::new();
+
+    for (wallet, quote) in quotes {
+        melts.spawn(async move {
+            let melt = wallet.melt(&quote.id).await;
+            (wallet, melt)
+        });
+    }
 
-        // Process payment
-        let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
-        println!("{quote:?}");
+    let melts = melts.join_all().await;
 
-        let melt = wallet.melt(&quote.id).await?;
-        println!("Paid invoice: {}", melt.state);
+    let mut error = false;
 
-        if let Some(preimage) = melt.preimage {
-            println!("Payment preimage: {preimage}");
+    for (wallet, melt) in melts {
+        match melt {
+            Ok(melt) => {
+                println!(
+                    "Melt for {} paid {} with fee of {} ",
+                    wallet.mint_url, melt.amount, melt.fee_paid
+                );
+            }
+            Err(err) => {
+                println!("Melt for {} failed with {}", wallet.mint_url, err);
+                error = true;
+            }
         }
     }
 
+    if error {
+        bail!("Could not complete all melts");
+    }
+
     Ok(())
 }

+ 101 - 24
crates/cdk-cli/src/sub_commands/mint.rs

@@ -4,7 +4,7 @@ use anyhow::{anyhow, Result};
 use cdk::amount::SplitTarget;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::ProofsMethods;
-use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
+use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, PaymentMethod};
 use cdk::wallet::{MultiMintWallet, WalletSubscription};
 use cdk::Amount;
 use clap::Args;
@@ -27,6 +27,15 @@ pub struct MintSubCommand {
     /// Quote Id
     #[arg(short, long)]
     quote_id: Option<String>,
+    /// Payment method
+    #[arg(long, default_value = "bolt11")]
+    method: String,
+    /// Expiry
+    #[arg(short, long)]
+    expiry: Option<u64>,
+    /// Expiry
+    #[arg(short, long)]
+    single_use: Option<bool>,
 }
 
 pub async fn mint(
@@ -39,36 +48,104 @@ pub async fn mint(
 
     let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?;
 
+    let mut payment_method = PaymentMethod::from_str(&sub_command_args.method)?;
+
     let quote_id = match &sub_command_args.quote_id {
-        None => {
-            let amount = sub_command_args
-                .amount
-                .ok_or(anyhow!("Amount must be defined"))?;
-            let quote = wallet.mint_quote(Amount::from(amount), description).await?;
-
-            println!("Quote: {quote:#?}");
-
-            println!("Please pay: {}", quote.request);
-
-            let mut subscription = wallet
-                .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
-                    .id
-                    .clone()]))
-                .await;
-
-            while let Some(msg) = subscription.recv().await {
-                if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
-                    if response.state == MintQuoteState::Paid {
-                        break;
+        None => match payment_method {
+            PaymentMethod::Bolt11 => {
+                let amount = sub_command_args
+                    .amount
+                    .ok_or(anyhow!("Amount must be defined"))?;
+                let quote = wallet.mint_quote(Amount::from(amount), description).await?;
+
+                println!("Quote: {quote:#?}");
+
+                println!("Please pay: {}", quote.request);
+
+                let mut subscription = wallet
+                    .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
+                        .id
+                        .clone()]))
+                    .await;
+
+                while let Some(msg) = subscription.recv().await {
+                    if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
+                        if response.state == MintQuoteState::Paid {
+                            break;
+                        }
+                    }
+                }
+                quote.id
+            }
+            PaymentMethod::Bolt12 => {
+                let amount = sub_command_args.amount;
+                println!("{:?}", sub_command_args.single_use);
+                let quote = wallet
+                    .mint_bolt12_quote(amount.map(|a| a.into()), description)
+                    .await?;
+
+                println!("Quote: {quote:#?}");
+
+                println!("Please pay: {}", quote.request);
+
+                let mut subscription = wallet
+                    .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
+                        .id
+                        .clone()]))
+                    .await;
+
+                while let Some(msg) = subscription.recv().await {
+                    if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
+                        if response.state == MintQuoteState::Paid {
+                            break;
+                        }
                     }
                 }
+                quote.id
+            }
+            _ => {
+                todo!()
             }
-            quote.id
+        },
+        Some(quote_id) => {
+            let quote = wallet
+                .localstore
+                .get_mint_quote(quote_id)
+                .await?
+                .ok_or(anyhow!("Unknown quote"))?;
+
+            payment_method = quote.payment_method;
+            quote_id.to_string()
         }
-        Some(quote_id) => quote_id.to_string(),
     };
 
-    let proofs = wallet.mint(&quote_id, SplitTarget::default(), None).await?;
+    tracing::debug!("Attempting mint for: {}", payment_method);
+
+    let proofs = match payment_method {
+        PaymentMethod::Bolt11 => wallet.mint(&quote_id, SplitTarget::default(), None).await?,
+        PaymentMethod::Bolt12 => {
+            let response = wallet.mint_bolt12_quote_state(&quote_id).await?;
+
+            let amount_mintable = response.amount_paid - response.amount_issued;
+
+            if amount_mintable == Amount::ZERO {
+                println!("Mint quote does not have amount that can be minted.");
+                return Ok(());
+            }
+
+            wallet
+                .mint_bolt12(
+                    &quote_id,
+                    Some(amount_mintable),
+                    SplitTarget::default(),
+                    None,
+                )
+                .await?
+        }
+        _ => {
+            todo!()
+        }
+    };
 
     let receive_amount = proofs.total_amount()?;
 

+ 6 - 0
crates/cdk-cln/src/error.rs

@@ -26,6 +26,12 @@ pub enum Error {
     /// Amount Error
     #[error(transparent)]
     Amount(#[from] cdk_common::amount::Error),
+    /// UTF-8 Error
+    #[error(transparent)]
+    Utf8(#[from] std::string::FromUtf8Error),
+    /// Bolt12 Error
+    #[error("Bolt12 error: {0}")]
+    Bolt12(String),
 }
 
 impl From<Error> for cdk_common::payment::Error {

+ 568 - 187
crates/cdk-cln/src/lib.rs

@@ -10,29 +10,35 @@ use std::pin::Pin;
 use std::str::FromStr;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
+use std::time::Duration;
 
 use async_trait::async_trait;
+use bitcoin::hashes::sha256::Hash;
 use cdk_common::amount::{to_unit, Amount};
 use cdk_common::common::FeeReserve;
-use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
+use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
-    PaymentQuoteResponse,
+    self, Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
+    CreateIncomingPaymentResponse, IncomingPaymentOptions, MakePaymentResponse, MintPayment,
+    OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, WaitPaymentResponse,
 };
 use cdk_common::util::{hex, unix_time};
-use cdk_common::{mint, Bolt11Invoice};
+use cdk_common::Bolt11Invoice;
 use cln_rpc::model::requests::{
-    InvoiceRequest, ListinvoicesRequest, ListpaysRequest, PayRequest, WaitanyinvoiceRequest,
+    DecodeRequest, FetchinvoiceRequest, InvoiceRequest, ListinvoicesRequest, ListpaysRequest,
+    OfferRequest, PayRequest, WaitanyinvoiceRequest,
 };
 use cln_rpc::model::responses::{
-    ListinvoicesInvoices, ListinvoicesInvoicesStatus, ListpaysPaysStatus, PayStatus,
-    WaitanyinvoiceStatus,
+    DecodeResponse, ListinvoicesInvoices, ListinvoicesInvoicesStatus, ListpaysPaysStatus,
+    PayStatus, WaitanyinvoiceResponse, WaitanyinvoiceStatus,
 };
-use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny};
+use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny, Sha256};
+use cln_rpc::ClnRpc;
 use error::Error;
 use futures::{Stream, StreamExt};
 use serde_json::Value;
 use tokio_util::sync::CancellationToken;
+use tracing::instrument;
 use uuid::Uuid;
 
 pub mod error;
@@ -68,6 +74,7 @@ impl MintPayment for Cln {
             unit: CurrencyUnit::Msat,
             invoice_description: true,
             amountless: true,
+            bolt12: true,
         })?)
     }
 
@@ -81,12 +88,33 @@ impl MintPayment for Cln {
         self.wait_invoice_cancel_token.cancel()
     }
 
+    #[instrument(skip_all)]
     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?;
-        let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
+    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
+        tracing::info!(
+            "CLN: Starting wait_any_incoming_payment with socket: {:?}",
+            self.rpc_socket
+        );
+
+        let last_pay_index = self.get_last_pay_index().await?.map(|idx| {
+            tracing::info!("CLN: Found last payment index: {}", idx);
+            idx
+        });
+
+        tracing::debug!("CLN: Connecting to CLN node...");
+        let cln_client = match cln_rpc::ClnRpc::new(&self.rpc_socket).await {
+            Ok(client) => {
+                tracing::debug!("CLN: Successfully connected to CLN node");
+                client
+            }
+            Err(err) => {
+                tracing::error!("CLN: Failed to connect to CLN node: {}", err);
+                return Err(Error::from(err).into());
+            }
+        };
 
+        tracing::debug!("CLN: Creating stream processing pipeline");
         let stream = futures::stream::unfold(
             (
                 cln_client,
@@ -97,70 +125,133 @@ impl MintPayment for Cln {
             |(mut cln_client, mut last_pay_idx, cancel_token, is_active)| async move {
                 // Set the stream as active
                 is_active.store(true, Ordering::SeqCst);
+                tracing::debug!("CLN: Stream is now active, waiting for invoice events with lastpay_index: {:?}", last_pay_idx);
 
                 loop {
-                    let request = WaitanyinvoiceRequest {
-                        timeout: None,
-                        lastpay_index: last_pay_idx,
-                    };
                     tokio::select! {
                         _ = cancel_token.cancelled() => {
                             // Set the stream as inactive
                             is_active.store(false, Ordering::SeqCst);
+                            tracing::info!("CLN: Invoice stream cancelled");
                             // End the stream
                             return None;
                         }
-                        result = cln_client.call_typed(&request) => {
+                        result = cln_client.call(cln_rpc::Request::WaitAnyInvoice(WaitanyinvoiceRequest {
+                            timeout: None,
+                            lastpay_index: last_pay_idx,
+                        })) => {
+                            tracing::debug!("CLN: Received response from WaitAnyInvoice call");
                             match result {
                                 Ok(invoice) => {
+                                    tracing::debug!("CLN: Successfully received invoice data");
+                                        // Try to convert the invoice to WaitanyinvoiceResponse
+                            let wait_any_response_result: Result<WaitanyinvoiceResponse, _> =
+                                invoice.try_into();
+
+                            let wait_any_response = match wait_any_response_result {
+                                Ok(response) => {
+                                    tracing::debug!("CLN: Parsed WaitAnyInvoice response successfully");
+                                    response
+                                }
+                                Err(e) => {
+                                    tracing::warn!(
+                                        "CLN: Failed to parse WaitAnyInvoice response: {:?}",
+                                        e
+                                    );
+                                    // Continue to the next iteration without panicking
+                                    continue;
+                                }
+                            };
 
                             // Check the status of the invoice
                             // We only want to yield invoices that have been paid
-                            match invoice.status {
-                                WaitanyinvoiceStatus::PAID => (),
-                                WaitanyinvoiceStatus::EXPIRED => continue,
+                            match wait_any_response.status {
+                                WaitanyinvoiceStatus::PAID => {
+                                    tracing::info!("CLN: Invoice with payment index {} is PAID", 
+                                                 wait_any_response.pay_index.unwrap_or_default());
+                                }
+                                WaitanyinvoiceStatus::EXPIRED => {
+                                    tracing::debug!("CLN: Invoice with payment index {} is EXPIRED, skipping", 
+                                                  wait_any_response.pay_index.unwrap_or_default());
+                                    continue;
+                                }
                             }
 
-                            last_pay_idx = invoice.pay_index;
+                            last_pay_idx = wait_any_response.pay_index;
+                            tracing::debug!("CLN: Updated last_pay_idx to {:?}", last_pay_idx);
+
+                            let payment_hash = wait_any_response.payment_hash;
+                            tracing::debug!("CLN: Payment hash: {}", payment_hash);
+
+                            let amount_msats = match wait_any_response.amount_received_msat {
+                                Some(amt) => {
+                                    tracing::info!("CLN: Received payment of {} msats for {}", 
+                                                 amt.msat(), payment_hash);
+                                    amt
+                                }
+                                None => {
+                                    tracing::error!("CLN: No amount in paid invoice, this should not happen");
+                                    continue;
+                                }
+                            };
+                            let amount_sats = amount_msats.msat() / 1000;
 
-                            let payment_hash = invoice.payment_hash.to_string();
+                            let payment_hash = Hash::from_bytes_ref(payment_hash.as_ref());
 
-                            let request_look_up = match invoice.bolt12 {
+                            let request_lookup_id = match wait_any_response.bolt12 {
                                 // If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up.
                                 // Since this is not returned in the wait any response,
                                 // we need to do a second query for it.
-                                Some(_) => {
+                                Some(bolt12) => {
+                                    tracing::info!("CLN: Processing BOLT12 payment, bolt12 value: {}", bolt12);
                                     match fetch_invoice_by_payment_hash(
                                         &mut cln_client,
-                                        &payment_hash,
+                                        payment_hash,
                                     )
                                     .await
                                     {
                                         Ok(Some(invoice)) => {
                                             if let Some(local_offer_id) = invoice.local_offer_id {
-                                                local_offer_id.to_string()
+                                                tracing::info!("CLN: Received bolt12 payment of {} sats for offer {}", 
+                                                             amount_sats, local_offer_id);
+                                                PaymentIdentifier::OfferId(local_offer_id.to_string())
                                             } else {
+                                                tracing::warn!("CLN: BOLT12 invoice has no local_offer_id, skipping");
                                                 continue;
                                             }
                                         }
-                                        Ok(None) => continue,
+                                        Ok(None) => {
+                                            tracing::warn!("CLN: Failed to find invoice by payment hash, skipping");
+                                            continue;
+                                        }
                                         Err(e) => {
                                             tracing::warn!(
-                                                "Error fetching invoice by payment hash: {e}"
+                                                "CLN: Error fetching invoice by payment hash: {e}"
                                             );
                                             continue;
                                         }
                                     }
                                 }
-                                None => payment_hash,
+                                None => {
+                                 tracing::info!("CLN: Processing BOLT11 payment with hash {}", payment_hash);
+                                 PaymentIdentifier::PaymentHash(*payment_hash.as_ref())
+                                },
+                            };
+
+                            let response = WaitPaymentResponse {
+                                payment_identifier: request_lookup_id,
+                                payment_amount: amount_sats.into(),
+                                unit: CurrencyUnit::Sat,
+                                payment_id: payment_hash.to_string()
                             };
+                            tracing::info!("CLN: Created WaitPaymentResponse with amount {} sats", amount_sats);
 
-                            return Some((request_look_up, (cln_client, last_pay_idx, cancel_token, is_active)));
+                            break Some((response, (cln_client, last_pay_idx, cancel_token, is_active)));
                                 }
                                 Err(e) => {
-                                    tracing::warn!("Error fetching invoice: {e}");
-                                    is_active.store(false, Ordering::SeqCst);
-                                    return None;
+                                    tracing::warn!("CLN: Error fetching invoice: {e}");
+                                    tokio::time::sleep(Duration::from_secs(1)).await;
+                                    continue;
                                 }
                             }
                         }
@@ -170,80 +261,199 @@ impl MintPayment for Cln {
         )
         .boxed();
 
+        tracing::info!("CLN: Successfully initialized invoice stream");
         Ok(stream)
     }
 
+    #[instrument(skip_all)]
     async fn get_payment_quote(
         &self,
-        request: &str,
         unit: &CurrencyUnit,
-        options: Option<MeltOptions>,
+        options: OutgoingPaymentOptions,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        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 = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+        match options {
+            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
+                // If we have specific amount options, use those
+                let amount_msat: Amount = if let Some(melt_options) = bolt11_options.melt_options {
+                    match melt_options {
+                        MeltOptions::Amountless { amountless } => {
+                            let amount_msat = amountless.amount_msat;
+
+                            if let Some(invoice_amount) =
+                                bolt11_options.bolt11.amount_milli_satoshis()
+                            {
+                                if !invoice_amount == u64::from(amount_msat) {
+                                    return Err(payment::Error::AmountMismatch);
+                                }
+                            }
+                            amount_msat
+                        }
+                        MeltOptions::Mpp { mpp } => mpp.amount,
+                    }
+                } else {
+                    // Fall back to invoice amount
+                    bolt11_options
+                        .bolt11
+                        .amount_milli_satoshis()
+                        .ok_or(Error::UnknownInvoiceAmount)?
+                        .into()
+                };
+                // Convert to target unit
+                let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+
+                // Calculate fee
+                let relative_fee_reserve =
+                    (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
+                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
+                let fee = max(relative_fee_reserve, absolute_fee_reserve);
+
+                Ok(PaymentQuoteResponse {
+                    request_lookup_id: PaymentIdentifier::PaymentHash(
+                        *bolt11_options.bolt11.payment_hash().as_ref(),
+                    ),
+                    amount,
+                    fee: fee.into(),
+                    state: MeltQuoteState::Unpaid,
+                    options: None,
+                    unit: unit.clone(),
+                })
+            }
+            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
+                let offer = bolt12_options.offer;
+
+                let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
+                    amount.amount_msat().into()
+                } else {
+                    // Fall back to offer amount
+                    let decode_response = self.decode_string(offer.to_string()).await?;
+
+                    decode_response
+                        .offer_amount_msat
+                        .ok_or(Error::UnknownInvoiceAmount)?
+                        .msat()
+                };
 
-        let relative_fee_reserve =
-            (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
+                // Convert to target unit
+                let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+
+                // Calculate fee
+                let relative_fee_reserve =
+                    (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
+                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
+                let fee = max(relative_fee_reserve, absolute_fee_reserve);
+
+                let cln_response;
+                {
+                    // Fetch invoice from offer
+                    let mut cln_client = self.cln_client().await?;
+
+                    cln_response = cln_client
+                        .call_typed(&FetchinvoiceRequest {
+                            amount_msat: Some(CLN_Amount::from_msat(amount_msat)),
+                            payer_metadata: None,
+                            payer_note: None,
+                            quantity: None,
+                            recurrence_counter: None,
+                            recurrence_label: None,
+                            recurrence_start: None,
+                            timeout: None,
+                            offer: offer.to_string(),
+                            bip353: None,
+                        })
+                        .await
+                        .map_err(|err| {
+                            tracing::error!("Could not fetch invoice for offer: {:?}", err);
+                            Error::ClnRpc(err)
+                        })?;
+                }
 
-        let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
+                let decode_response = self.decode_string(cln_response.invoice.clone()).await?;
 
-        let fee = max(relative_fee_reserve, absolute_fee_reserve);
+                let options = payment::PaymentQuoteOptions::Bolt12 {
+                    invoice: Some(cln_response.invoice.into()),
+                };
 
-        Ok(PaymentQuoteResponse {
-            request_lookup_id: bolt11.payment_hash().to_string(),
-            amount,
-            unit: unit.clone(),
-            fee: fee.into(),
-            state: MeltQuoteState::Unpaid,
-        })
+                Ok(PaymentQuoteResponse {
+                    request_lookup_id: PaymentIdentifier::Bolt12PaymentHash(
+                        hex::decode(
+                            decode_response
+                                .invoice_payment_hash
+                                .ok_or(Error::UnknownInvoice)?,
+                        )
+                        .unwrap()
+                        .try_into()
+                        .map_err(|_| Error::InvalidHash)?,
+                    ),
+                    amount,
+                    fee: fee.into(),
+                    state: MeltQuoteState::Unpaid,
+                    options: Some(options),
+                    unit: unit.clone(),
+                })
+            }
+        }
     }
 
+    #[instrument(skip_all)]
     async fn make_payment(
         &self,
-        melt_quote: mint::MeltQuote,
-        partial_amount: Option<Amount>,
-        max_fee: Option<Amount>,
+        unit: &CurrencyUnit,
+        options: OutgoingPaymentOptions,
     ) -> Result<MakePaymentResponse, Self::Err> {
-        let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
-        let pay_state = self
-            .check_outgoing_payment(&bolt11.payment_hash().to_string())
-            .await?;
+        let max_fee_msat: Option<u64>;
+        let mut partial_amount: Option<u64> = None;
+        let mut amount_msat: Option<u64> = None;
+
+        let invoice = match options {
+            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
+                let payment_identifier =
+                    PaymentIdentifier::PaymentHash(*bolt11_options.bolt11.payment_hash().as_ref());
+
+                self.check_outgoing_unpaided(&payment_identifier).await?;
+
+                if let Some(melt_options) = bolt11_options.melt_options {
+                    match melt_options {
+                        MeltOptions::Mpp { mpp } => partial_amount = Some(mpp.amount.into()),
+                        MeltOptions::Amountless { amountless } => {
+                            amount_msat = Some(amountless.amount_msat.into());
+                        }
+                    }
+                }
 
-        match pay_state.status {
-            MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
-            MeltQuoteState::Paid => {
-                tracing::debug!("Melt attempted on invoice already paid");
-                return Err(Self::Err::InvoiceAlreadyPaid);
+                max_fee_msat = bolt11_options.max_fee_amount.map(|a| a.into());
+
+                bolt11_options.bolt11.to_string()
             }
-            MeltQuoteState::Pending => {
-                tracing::debug!("Melt attempted on invoice already pending");
-                return Err(Self::Err::InvoicePaymentPending);
+            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
+                let bolt12_invoice = bolt12_options.invoice.ok_or(Error::UnknownInvoice)?;
+                let decode_response = self
+                    .decode_string(String::from_utf8(bolt12_invoice.clone()).map_err(Error::Utf8)?)
+                    .await?;
+
+                let payment_identifier = PaymentIdentifier::Bolt12PaymentHash(
+                    hex::decode(
+                        decode_response
+                            .invoice_payment_hash
+                            .ok_or(Error::UnknownInvoice)?,
+                    )
+                    .map_err(|e| Error::Bolt12(e.to_string()))?
+                    .try_into()
+                    .map_err(|_| Error::InvalidHash)?,
+                );
+
+                self.check_outgoing_unpaided(&payment_identifier).await?;
+
+                max_fee_msat = bolt12_options.max_fee_amount.map(|a| a.into());
+                String::from_utf8(bolt12_invoice).map_err(Error::Utf8)?
             }
-        }
+        };
 
-        let amount_msat = partial_amount
-            .is_none()
-            .then(|| {
-                melt_quote
-                    .msat_to_pay
-                    .map(|a| CLN_Amount::from_msat(a.into()))
-            })
-            .flatten();
+        let mut cln_client = self.cln_client().await?;
 
-        let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
         let cln_response = cln_client
             .call_typed(&PayRequest {
-                bolt11: melt_quote.request.to_string(),
-                amount_msat,
+                bolt11: invoice,
+                amount_msat: amount_msat.map(CLN_Amount::from_msat),
                 label: None,
                 riskfactor: None,
                 maxfeepercent: None,
@@ -252,22 +462,9 @@ impl MintPayment for Cln {
                 exemptfee: None,
                 localinvreqid: None,
                 exclude: None,
-                maxfee: max_fee
-                    .map(|a| {
-                        let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
-                        Ok::<CLN_Amount, Self::Err>(CLN_Amount::from_msat(msat.into()))
-                    })
-                    .transpose()?,
+                maxfee: max_fee_msat.map(CLN_Amount::from_msat),
                 description: None,
-                partial_msat: partial_amount
-                    .map(|a| {
-                        let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
-
-                        Ok::<cln_rpc::primitives::Amount, Self::Err>(CLN_Amount::from_msat(
-                            msat.into(),
-                        ))
-                    })
-                    .transpose()?,
+                partial_msat: partial_amount.map(CLN_Amount::from_msat),
             })
             .await;
 
@@ -279,16 +476,19 @@ impl MintPayment for Cln {
                     PayStatus::FAILED => MeltQuoteState::Failed,
                 };
 
+                let payment_identifier =
+                    PaymentIdentifier::PaymentHash(*pay_response.payment_hash.as_ref());
+
                 MakePaymentResponse {
                     payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())),
-                    payment_lookup_id: pay_response.payment_hash.to_string(),
+                    payment_lookup_id: payment_identifier,
                     status,
                     total_spent: to_unit(
                         pay_response.amount_sent_msat.msat(),
                         &CurrencyUnit::Msat,
-                        &melt_quote.unit,
+                        unit,
                     )?,
-                    unit: melt_quote.unit,
+                    unit: unit.clone(),
                 }
             }
             Err(err) => {
@@ -300,90 +500,206 @@ impl MintPayment for Cln {
         Ok(response)
     }
 
+    #[instrument(skip_all)]
     async fn create_incoming_payment_request(
         &self,
-        amount: Amount,
         unit: &CurrencyUnit,
-        description: String,
-        unix_expiry: Option<u64>,
+        options: IncomingPaymentOptions,
     ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
-        let time_now = unix_time();
-
-        let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
-
-        let label = Uuid::new_v4().to_string();
-
-        let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
-        let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount.into()));
-
-        let invoice_response = cln_client
-            .call_typed(&InvoiceRequest {
-                amount_msat,
+        match options {
+            IncomingPaymentOptions::Bolt11(Bolt11IncomingPaymentOptions {
                 description,
-                label: label.clone(),
-                expiry: unix_expiry.map(|t| t - time_now),
-                fallbacks: None,
-                preimage: None,
-                cltv: None,
-                deschashonly: None,
-                exposeprivatechannels: None,
-            })
-            .await
-            .map_err(Error::from)?;
+                amount,
+                unix_expiry,
+            }) => {
+                let time_now = unix_time();
+
+                let mut cln_client = self.cln_client().await?;
+
+                let label = Uuid::new_v4().to_string();
+
+                let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
+                let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount.into()));
+
+                let invoice_response = cln_client
+                    .call_typed(&InvoiceRequest {
+                        amount_msat,
+                        description: description.unwrap_or_default(),
+                        label: label.clone(),
+                        expiry: unix_expiry.map(|t| t - time_now),
+                        fallbacks: None,
+                        preimage: None,
+                        cltv: None,
+                        deschashonly: None,
+                        exposeprivatechannels: None,
+                    })
+                    .await
+                    .map_err(Error::from)?;
 
-        let request = Bolt11Invoice::from_str(&invoice_response.bolt11)?;
-        let expiry = request.expires_at().map(|t| t.as_secs());
-        let payment_hash = request.payment_hash();
+                let request = Bolt11Invoice::from_str(&invoice_response.bolt11)?;
+                let expiry = request.expires_at().map(|t| t.as_secs());
+                let payment_hash = request.payment_hash();
 
-        Ok(CreateIncomingPaymentResponse {
-            request_lookup_id: payment_hash.to_string(),
-            request: request.to_string(),
-            expiry,
-        })
+                Ok(CreateIncomingPaymentResponse {
+                    request_lookup_id: PaymentIdentifier::PaymentHash(*payment_hash.as_ref()),
+                    request: request.to_string(),
+                    expiry,
+                })
+            }
+            IncomingPaymentOptions::Bolt12(bolt12_options) => {
+                let Bolt12IncomingPaymentOptions {
+                    description,
+                    amount,
+                    unix_expiry,
+                } = *bolt12_options;
+                let mut cln_client = self.cln_client().await?;
+
+                let label = Uuid::new_v4().to_string();
+
+                // Match like this until we change to option
+                let amount = match amount {
+                    Some(amount) => {
+                        let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
+
+                        amount.to_string()
+                    }
+                    None => "any".to_string(),
+                };
+
+                // It seems that the only way to force cln to create a unique offer
+                // is to encode some random data in the offer
+                let issuer = Uuid::new_v4().to_string();
+
+                let offer_response = cln_client
+                    .call_typed(&OfferRequest {
+                        amount,
+                        absolute_expiry: unix_expiry,
+                        description: Some(description.unwrap_or_default()),
+                        issuer: Some(issuer.to_string()),
+                        label: Some(label.to_string()),
+                        single_use: None,
+                        quantity_max: None,
+                        recurrence: None,
+                        recurrence_base: None,
+                        recurrence_limit: None,
+                        recurrence_paywindow: None,
+                        recurrence_start_any_period: None,
+                    })
+                    .await
+                    .map_err(Error::from)?;
+
+                Ok(CreateIncomingPaymentResponse {
+                    request_lookup_id: PaymentIdentifier::OfferId(
+                        offer_response.offer_id.to_string(),
+                    ),
+                    request: offer_response.bolt12,
+                    expiry: unix_expiry,
+                })
+            }
+        }
     }
 
+    #[instrument(skip(self))]
     async fn check_incoming_payment_status(
         &self,
-        payment_hash: &str,
-    ) -> Result<MintQuoteState, Self::Err> {
-        let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
-
-        let listinvoices_response = cln_client
-            .call_typed(&ListinvoicesRequest {
-                payment_hash: Some(payment_hash.to_string()),
-                label: None,
-                invstring: None,
-                offer_id: None,
-                index: None,
-                limit: None,
-                start: None,
-            })
-            .await
-            .map_err(Error::from)?;
-
-        let status = match listinvoices_response.invoices.first() {
-            Some(invoice_response) => cln_invoice_status_to_mint_state(invoice_response.status),
-            None => {
-                tracing::info!(
-                    "Check invoice called on unknown look up id: {}",
-                    payment_hash
-                );
-                return Err(Error::WrongClnResponse.into());
+        payment_identifier: &PaymentIdentifier,
+    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
+        let mut cln_client = self.cln_client().await?;
+
+        let listinvoices_response = match payment_identifier {
+            PaymentIdentifier::Label(label) => {
+                // Query by label
+                cln_client
+                    .call_typed(&ListinvoicesRequest {
+                        payment_hash: None,
+                        label: Some(label.to_string()),
+                        invstring: None,
+                        offer_id: None,
+                        index: None,
+                        limit: None,
+                        start: None,
+                    })
+                    .await
+                    .map_err(Error::from)?
+            }
+            PaymentIdentifier::OfferId(offer_id) => {
+                // Query by offer_id
+                cln_client
+                    .call_typed(&ListinvoicesRequest {
+                        payment_hash: None,
+                        label: None,
+                        invstring: None,
+                        offer_id: Some(offer_id.to_string()),
+                        index: None,
+                        limit: None,
+                        start: None,
+                    })
+                    .await
+                    .map_err(Error::from)?
+            }
+            PaymentIdentifier::PaymentHash(payment_hash) => {
+                // Query by payment_hash
+                cln_client
+                    .call_typed(&ListinvoicesRequest {
+                        payment_hash: Some(hex::encode(payment_hash)),
+                        label: None,
+                        invstring: None,
+                        offer_id: None,
+                        index: None,
+                        limit: None,
+                        start: None,
+                    })
+                    .await
+                    .map_err(Error::from)?
+            }
+            PaymentIdentifier::CustomId(_) => {
+                tracing::error!("Unsupported payment id for CLN");
+                return Err(payment::Error::UnknownPaymentState);
+            }
+            PaymentIdentifier::Bolt12PaymentHash(_) => {
+                tracing::error!("Unsupported payment id for CLN");
+                return Err(payment::Error::UnknownPaymentState);
             }
         };
 
-        Ok(status)
+        Ok(listinvoices_response
+            .invoices
+            .iter()
+            .filter(|p| p.status == ListinvoicesInvoicesStatus::PAID)
+            .filter(|p| p.amount_msat.is_some()) // Filter out invoices without an amount
+            .map(|p| WaitPaymentResponse {
+                payment_identifier: payment_identifier.clone(),
+                payment_amount: p
+                    .amount_msat
+                    // Safe to expect since we filtered for Some
+                    .expect("We have filter out those without amounts")
+                    .msat()
+                    .into(),
+                unit: CurrencyUnit::Msat,
+                payment_id: p.payment_hash.to_string(),
+            })
+            .collect())
     }
 
+    #[instrument(skip(self))]
     async fn check_outgoing_payment(
         &self,
-        payment_hash: &str,
+        payment_identifier: &PaymentIdentifier,
     ) -> Result<MakePaymentResponse, Self::Err> {
-        let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
+        let mut cln_client = self.cln_client().await?;
+
+        let payment_hash = match payment_identifier {
+            PaymentIdentifier::PaymentHash(hash) => hash,
+            PaymentIdentifier::Bolt12PaymentHash(hash) => hash,
+            _ => {
+                tracing::error!("Unsupported identifier to check outgoing payment for cln.");
+                return Err(payment::Error::UnknownPaymentState);
+            }
+        };
 
         let listpays_response = cln_client
             .call_typed(&ListpaysRequest {
-                payment_hash: Some(payment_hash.parse().map_err(|_| Error::InvalidHash)?),
+                payment_hash: Some(*Sha256::from_bytes_ref(payment_hash)),
                 bolt11: None,
                 status: None,
                 start: None,
@@ -398,7 +714,7 @@ impl MintPayment for Cln {
                 let status = cln_pays_status_to_mint_state(pays_response.status);
 
                 Ok(MakePaymentResponse {
-                    payment_lookup_id: pays_response.payment_hash.to_string(),
+                    payment_lookup_id: payment_identifier.clone(),
                     payment_proof: pays_response.preimage.map(|p| hex::encode(p.to_vec())),
                     status,
                     total_spent: pays_response
@@ -408,7 +724,7 @@ impl MintPayment for Cln {
                 })
             }
             None => Ok(MakePaymentResponse {
-                payment_lookup_id: payment_hash.to_string(),
+                payment_lookup_id: payment_identifier.clone(),
                 payment_proof: None,
                 status: MeltQuoteState::Unknown,
                 total_spent: Amount::ZERO,
@@ -419,9 +735,13 @@ impl MintPayment for Cln {
 }
 
 impl Cln {
+    async fn cln_client(&self) -> Result<ClnRpc, Error> {
+        Ok(cln_rpc::ClnRpc::new(&self.rpc_socket).await?)
+    }
+
     /// Get last pay index for cln
     async fn get_last_pay_index(&self) -> Result<Option<u64>, Error> {
-        let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
+        let mut cln_client = self.cln_client().await?;
         let listinvoices_response = cln_client
             .call_typed(&ListinvoicesRequest {
                 index: None,
@@ -440,13 +760,40 @@ impl Cln {
             None => Ok(None),
         }
     }
-}
 
-fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQuoteState {
-    match status {
-        ListinvoicesInvoicesStatus::UNPAID => MintQuoteState::Unpaid,
-        ListinvoicesInvoicesStatus::PAID => MintQuoteState::Paid,
-        ListinvoicesInvoicesStatus::EXPIRED => MintQuoteState::Unpaid,
+    /// Decode string    
+    #[instrument(skip(self))]
+    async fn decode_string(&self, string: String) -> Result<DecodeResponse, Error> {
+        let mut cln_client = self.cln_client().await?;
+
+        cln_client
+            .call_typed(&DecodeRequest { string })
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not fetch invoice for offer: {:?}", err);
+                Error::ClnRpc(err)
+            })
+    }
+
+    /// Checks that outgoing payment is not already paid
+    #[instrument(skip(self))]
+    async fn check_outgoing_unpaided(
+        &self,
+        payment_identifier: &PaymentIdentifier,
+    ) -> Result<(), payment::Error> {
+        let pay_state = self.check_outgoing_payment(payment_identifier).await?;
+
+        match pay_state.status {
+            MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => Ok(()),
+            MeltQuoteState::Paid => {
+                tracing::debug!("Melt attempted on invoice already paid");
+                Err(payment::Error::InvoiceAlreadyPaid)
+            }
+            MeltQuoteState::Pending => {
+                tracing::debug!("Melt attempted on invoice already pending");
+                Err(payment::Error::InvoicePaymentPending)
+            }
+        }
     }
 }
 
@@ -460,23 +807,57 @@ fn cln_pays_status_to_mint_state(status: ListpaysPaysStatus) -> MeltQuoteState {
 
 async fn fetch_invoice_by_payment_hash(
     cln_client: &mut cln_rpc::ClnRpc,
-    payment_hash: &str,
+    payment_hash: &Hash,
 ) -> Result<Option<ListinvoicesInvoices>, Error> {
-    match cln_client
-        .call_typed(&ListinvoicesRequest {
-            payment_hash: Some(payment_hash.to_string()),
-            index: None,
-            invstring: None,
-            label: None,
-            limit: None,
-            offer_id: None,
-            start: None,
-        })
-        .await
-    {
-        Ok(invoice_response) => Ok(invoice_response.invoices.first().cloned()),
+    tracing::debug!("Fetching invoice by payment hash: {}", payment_hash);
+
+    let payment_hash_str = payment_hash.to_string();
+    tracing::debug!("Payment hash string: {}", payment_hash_str);
+
+    let request = ListinvoicesRequest {
+        payment_hash: Some(payment_hash_str),
+        index: None,
+        invstring: None,
+        label: None,
+        limit: None,
+        offer_id: None,
+        start: None,
+    };
+    tracing::debug!("Created ListinvoicesRequest");
+
+    match cln_client.call_typed(&request).await {
+        Ok(invoice_response) => {
+            let invoice_count = invoice_response.invoices.len();
+            tracing::debug!(
+                "Received {} invoices for payment hash {}",
+                invoice_count,
+                payment_hash
+            );
+
+            if invoice_count > 0 {
+                let first_invoice = invoice_response.invoices.first().cloned();
+                if let Some(invoice) = &first_invoice {
+                    tracing::debug!("Found invoice with payment hash {}", payment_hash);
+                    tracing::debug!(
+                        "Invoice details - local_offer_id: {:?}, status: {:?}",
+                        invoice.local_offer_id,
+                        invoice.status
+                    );
+                } else {
+                    tracing::warn!("No invoice found with payment hash {}", payment_hash);
+                }
+                Ok(first_invoice)
+            } else {
+                tracing::warn!("No invoices returned for payment hash {}", payment_hash);
+                Ok(None)
+            }
+        }
         Err(e) => {
-            tracing::warn!("Error fetching invoice: {e}");
+            tracing::error!(
+                "Error fetching invoice by payment hash {}: {}",
+                payment_hash,
+                e
+            );
             Err(Error::from(e))
         }
     }

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

@@ -27,6 +27,7 @@ cbor-diag.workspace = true
 ciborium.workspace = true
 serde.workspace = true
 lightning-invoice.workspace = true
+lightning.workspace = true
 thiserror.workspace = true
 tracing.workspace = true
 url.workspace = true

+ 24 - 15
crates/cdk-common/src/database/mint/mod.rs

@@ -3,16 +3,16 @@
 use std::collections::HashMap;
 
 use async_trait::async_trait;
-use cashu::MintInfo;
+use cashu::{Amount, MintInfo};
 use uuid::Uuid;
 
 use super::Error;
 use crate::common::QuoteTTL;
 use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote};
 use crate::nuts::{
-    BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
-    State,
+    BlindSignature, CurrencyUnit, Id, MeltQuoteState, Proof, Proofs, PublicKey, State,
 };
+use crate::payment::PaymentIdentifier;
 
 #[cfg(feature = "auth")]
 mod auth;
@@ -67,13 +67,20 @@ pub trait QuotesTransaction<'a> {
     async fn get_mint_quote(&mut self, quote_id: &Uuid)
         -> Result<Option<MintMintQuote>, Self::Err>;
     /// Add [`MintMintQuote`]
-    async fn add_or_replace_mint_quote(&mut self, quote: MintMintQuote) -> Result<(), Self::Err>;
-    /// Update state of [`MintMintQuote`]
-    async fn update_mint_quote_state(
+    async fn add_mint_quote(&mut self, quote: MintMintQuote) -> Result<(), Self::Err>;
+    /// Increment amount paid [`MintMintQuote`]
+    async fn increment_mint_quote_amount_paid(
         &mut self,
         quote_id: &Uuid,
-        state: MintQuoteState,
-    ) -> Result<MintQuoteState, Self::Err>;
+        amount_paid: Amount,
+        payment_id: String,
+    ) -> Result<Amount, Self::Err>;
+    /// Increment amount paid [`MintMintQuote`]
+    async fn increment_mint_quote_amount_issued(
+        &mut self,
+        quote_id: &Uuid,
+        amount_issued: Amount,
+    ) -> Result<Amount, Self::Err>;
     /// Remove [`MintMintQuote`]
     async fn remove_mint_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err>;
     /// Get [`mint::MeltQuote`] and lock it for update in this transaction
@@ -88,7 +95,7 @@ pub trait QuotesTransaction<'a> {
     async fn update_melt_quote_request_lookup_id(
         &mut self,
         quote_id: &Uuid,
-        new_request_lookup_id: &str,
+        new_request_lookup_id: &PaymentIdentifier,
     ) -> Result<(), Self::Err>;
 
     /// Update [`mint::MeltQuote`] state
@@ -98,6 +105,7 @@ pub trait QuotesTransaction<'a> {
         &mut self,
         quote_id: &Uuid,
         new_state: MeltQuoteState,
+        payment_proof: Option<String>,
     ) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err>;
     /// Remove [`mint::MeltQuote`]
     async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err>;
@@ -106,6 +114,12 @@ pub trait QuotesTransaction<'a> {
         &mut self,
         request: &str,
     ) -> Result<Option<MintMintQuote>, Self::Err>;
+
+    /// Get all [`MintMintQuote`]s
+    async fn get_mint_quote_by_request_lookup_id(
+        &mut self,
+        request_lookup_id: &PaymentIdentifier,
+    ) -> Result<Option<MintMintQuote>, Self::Err>;
 }
 
 /// Mint Quote Database trait
@@ -125,15 +139,10 @@ pub trait QuotesDatabase {
     /// Get all [`MintMintQuote`]s
     async fn get_mint_quote_by_request_lookup_id(
         &self,
-        request_lookup_id: &str,
+        request_lookup_id: &PaymentIdentifier,
     ) -> Result<Option<MintMintQuote>, Self::Err>;
     /// Get Mint Quotes
     async fn get_mint_quotes(&self) -> Result<Vec<MintMintQuote>, Self::Err>;
-    /// Get Mint Quotes with state
-    async fn get_mint_quotes_with_state(
-        &self,
-        state: MintQuoteState,
-    ) -> Result<Vec<MintMintQuote>, Self::Err>;
     /// Get [`mint::MeltQuote`]
     async fn get_melt_quote(&self, quote_id: &Uuid) -> Result<Option<mint::MeltQuote>, Self::Err>;
     /// Get all [`mint::MeltQuote`]s

+ 3 - 0
crates/cdk-common/src/database/mod.rs

@@ -29,6 +29,9 @@ pub enum Error {
     /// Duplicate entry
     #[error("Duplicate entry")]
     Duplicate,
+    /// Amount overflow
+    #[error("Amount overflow")]
+    AmountOverflow,
 
     /// DHKE error
     #[error(transparent)]

+ 18 - 0
crates/cdk-common/src/error.rs

@@ -91,6 +91,24 @@ pub enum Error {
     /// Multi-Part Payment not supported for unit and method
     #[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")]
     AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod),
+    /// Duplicate Payment id
+    #[error("Payment id seen for mint")]
+    DuplicatePaymentId,
+    /// Pubkey required
+    #[error("Pubkey required")]
+    PubkeyRequired,
+    /// Invalid payment method
+    #[error("Invalid payment method")]
+    InvalidPaymentMethod,
+    /// Amount undefined
+    #[error("Amount undefined")]
+    AmountUndefined,
+    /// Unsupported payment method
+    #[error("Payment method unsupported")]
+    UnsupportedPaymentMethod,
+    /// Could not parse bolt12
+    #[error("Could not parse bolt12")]
+    Bolt12parse,
 
     /// Internal Error - Send error
     #[error("Internal send error: {0}")]

+ 2 - 0
crates/cdk-common/src/lib.rs

@@ -12,6 +12,8 @@ pub mod common;
 pub mod database;
 pub mod error;
 #[cfg(feature = "mint")]
+pub mod melt;
+#[cfg(feature = "mint")]
 pub mod mint;
 #[cfg(feature = "mint")]
 pub mod payment;

+ 26 - 0
crates/cdk-common/src/melt.rs

@@ -0,0 +1,26 @@
+//! Melt types
+use cashu::{MeltQuoteBolt11Request, MeltQuoteBolt12Request};
+
+/// Melt quote request enum for different types of quotes
+///
+/// This enum represents the different types of melt quote requests
+/// that can be made, either BOLT11 or BOLT12.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MeltQuoteRequest {
+    /// Lightning Network BOLT11 invoice request
+    Bolt11(MeltQuoteBolt11Request),
+    /// Lightning Network BOLT12 offer request
+    Bolt12(MeltQuoteBolt12Request),
+}
+
+impl From<MeltQuoteBolt11Request> for MeltQuoteRequest {
+    fn from(request: MeltQuoteBolt11Request) -> Self {
+        MeltQuoteRequest::Bolt11(request)
+    }
+}
+
+impl From<MeltQuoteBolt12Request> for MeltQuoteRequest {
+    fn from(request: MeltQuoteBolt12Request) -> Self {
+        MeltQuoteRequest::Bolt12(request)
+    }
+}

+ 283 - 28
crates/cdk-common/src/mint.rs

@@ -2,11 +2,17 @@
 
 use bitcoin::bip32::DerivationPath;
 use cashu::util::unix_time;
-use cashu::{MeltQuoteBolt11Response, MintQuoteBolt11Response};
+use cashu::{
+    Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MintQuoteBolt11Response,
+    MintQuoteBolt12Response, PaymentMethod,
+};
+use lightning::offers::offer::Offer;
 use serde::{Deserialize, Serialize};
+use tracing::instrument;
 use uuid::Uuid;
 
 use crate::nuts::{MeltQuoteState, MintQuoteState};
+use crate::payment::PaymentIdentifier;
 use crate::{Amount, CurrencyUnit, Id, KeySetInfo, PublicKey};
 
 /// Mint Quote Info
@@ -15,56 +21,211 @@ pub struct MintQuote {
     /// Quote id
     pub id: Uuid,
     /// Amount of quote
-    pub amount: Amount,
+    pub amount: Option<Amount>,
     /// Unit of quote
     pub unit: CurrencyUnit,
     /// Quote payment request e.g. bolt11
     pub request: String,
-    /// Quote state
-    pub state: MintQuoteState,
     /// Expiration time of quote
     pub expiry: u64,
     /// Value used by ln backend to look up state of request
-    pub request_lookup_id: String,
+    pub request_lookup_id: PaymentIdentifier,
     /// Pubkey
     pub pubkey: Option<PublicKey>,
     /// Unix time quote was created
     #[serde(default)]
     pub created_time: u64,
-    /// Unix time quote was paid
-    pub paid_time: Option<u64>,
-    /// Unix time quote was issued
-    pub issued_time: Option<u64>,
+    /// Amount paid
+    #[serde(default)]
+    amount_paid: Amount,
+    /// Amount issued
+    #[serde(default)]
+    amount_issued: Amount,
+    /// Payment of payment(s) that filled quote
+    #[serde(default)]
+    pub payments: Vec<IncomingPayment>,
+    /// Payment Method
+    #[serde(default)]
+    pub payment_method: PaymentMethod,
+    /// Payment of payment(s) that filled quote
+    #[serde(default)]
+    pub issuance: Vec<Issuance>,
 }
 
 impl MintQuote {
     /// Create new [`MintQuote`]
+    #[allow(clippy::too_many_arguments)]
     pub fn new(
+        id: Option<Uuid>,
         request: String,
         unit: CurrencyUnit,
-        amount: Amount,
+        amount: Option<Amount>,
         expiry: u64,
-        request_lookup_id: String,
+        request_lookup_id: PaymentIdentifier,
         pubkey: Option<PublicKey>,
+        amount_paid: Amount,
+        amount_issued: Amount,
+        payment_method: PaymentMethod,
+        created_time: u64,
+        payments: Vec<IncomingPayment>,
+        issuance: Vec<Issuance>,
     ) -> Self {
-        let id = Uuid::new_v4();
+        let id = id.unwrap_or(Uuid::new_v4());
 
         Self {
             id,
             amount,
             unit,
             request,
-            state: MintQuoteState::Unpaid,
             expiry,
             request_lookup_id,
             pubkey,
-            created_time: unix_time(),
-            paid_time: None,
-            issued_time: None,
+            created_time,
+            amount_paid,
+            amount_issued,
+            payment_method,
+            payments,
+            issuance,
+        }
+    }
+
+    /// Increment the amount paid on the mint quote by a given amount
+    #[instrument(skip(self))]
+    pub fn increment_amount_paid(
+        &mut self,
+        additional_amount: Amount,
+    ) -> Result<Amount, crate::Error> {
+        self.amount_paid = self
+            .amount_paid
+            .checked_add(additional_amount)
+            .ok_or(crate::Error::AmountOverflow)?;
+        Ok(self.amount_paid)
+    }
+
+    /// Amount paid
+    #[instrument(skip(self))]
+    pub fn amount_paid(&self) -> Amount {
+        self.amount_paid
+    }
+
+    /// Increment the amount issued on the mint quote by a given amount
+    #[instrument(skip(self))]
+    pub fn increment_amount_issued(
+        &mut self,
+        additional_amount: Amount,
+    ) -> Result<Amount, crate::Error> {
+        self.amount_issued = self
+            .amount_issued
+            .checked_add(additional_amount)
+            .ok_or(crate::Error::AmountOverflow)?;
+        Ok(self.amount_issued)
+    }
+
+    /// Amount issued
+    #[instrument(skip(self))]
+    pub fn amount_issued(&self) -> Amount {
+        self.amount_issued
+    }
+
+    /// Get state of mint quote
+    #[instrument(skip(self))]
+    pub fn state(&self) -> MintQuoteState {
+        self.compute_quote_state()
+    }
+
+    /// Existing payment ids of a mint quote
+    pub fn payment_ids(&self) -> Vec<&String> {
+        self.payments.iter().map(|a| &a.payment_id).collect()
+    }
+
+    /// Add a payment ID to the list of payment IDs
+    ///
+    /// Returns an error if the payment ID is already in the list
+    #[instrument(skip(self))]
+    pub fn add_payment(
+        &mut self,
+        amount: Amount,
+        payment_id: String,
+        time: u64,
+    ) -> Result<(), crate::Error> {
+        let payment_ids = self.payment_ids();
+        if payment_ids.contains(&&payment_id) {
+            return Err(crate::Error::DuplicatePaymentId);
+        }
+
+        let payment = IncomingPayment::new(amount, payment_id, time);
+
+        self.payments.push(payment);
+        Ok(())
+    }
+
+    /// Compute quote state
+    #[instrument(skip(self))]
+    fn compute_quote_state(&self) -> MintQuoteState {
+        if self.amount_paid == Amount::ZERO && self.amount_issued == Amount::ZERO {
+            return MintQuoteState::Unpaid;
+        }
+
+        match self.amount_paid.cmp(&self.amount_issued) {
+            std::cmp::Ordering::Less => {
+                // self.amount_paid is less than other (amount issued)
+                // Handle case where paid amount is insufficient
+                tracing::error!("We should not have issued more then has been paid");
+                MintQuoteState::Issued
+            }
+            std::cmp::Ordering::Equal => {
+                // We do this extra check for backwards compatibility for quotes where amount paid/issed was not tracked
+                // self.amount_paid equals other (amount issued)
+                // Handle case where paid amount exactly matches
+                MintQuoteState::Issued
+            }
+            std::cmp::Ordering::Greater => {
+                // self.amount_paid is greater than other (amount issued)
+                // Handle case where paid amount exceeds required amount
+                MintQuoteState::Paid
+            }
+        }
+    }
+}
+
+/// Mint Payments
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct IncomingPayment {
+    /// Amount
+    pub amount: Amount,
+    /// Pyament unix time
+    pub time: u64,
+    /// Payment id
+    pub payment_id: String,
+}
+
+impl IncomingPayment {
+    /// New [`IncomingPayment`]
+    pub fn new(amount: Amount, payment_id: String, time: u64) -> Self {
+        Self {
+            payment_id,
+            time,
+            amount,
         }
     }
 }
 
+/// Informattion about issued quote
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Issuance {
+    /// Amount
+    pub amount: Amount,
+    /// Time
+    pub time: u64,
+}
+
+impl Issuance {
+    /// Create new [`Issuance`]
+    pub fn new(amount: Amount, time: u64) -> Self {
+        Self { amount, time }
+    }
+}
+
 /// Melt Quote Info
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MeltQuote {
@@ -75,7 +236,7 @@ pub struct MeltQuote {
     /// Quote amount
     pub amount: Amount,
     /// Quote Payment request e.g. bolt11
-    pub request: String,
+    pub request: MeltPaymentRequest,
     /// Quote fee reserve
     pub fee_reserve: Amount,
     /// Quote state
@@ -85,28 +246,33 @@ pub struct MeltQuote {
     /// Payment preimage
     pub payment_preimage: Option<String>,
     /// Value used by ln backend to look up state of request
-    pub request_lookup_id: String,
-    /// Msat to pay
+    pub request_lookup_id: PaymentIdentifier,
+    /// Payment options
     ///
-    /// Used for an amountless invoice
-    pub msat_to_pay: Option<Amount>,
+    /// Used for amountless invoices and MPP payments
+    pub options: Option<MeltOptions>,
     /// Unix time quote was created
     #[serde(default)]
     pub created_time: u64,
     /// Unix time quote was paid
     pub paid_time: Option<u64>,
+    /// Payment method
+    #[serde(default)]
+    pub payment_method: PaymentMethod,
 }
 
 impl MeltQuote {
     /// Create new [`MeltQuote`]
+    #[allow(clippy::too_many_arguments)]
     pub fn new(
-        request: String,
+        request: MeltPaymentRequest,
         unit: CurrencyUnit,
         amount: Amount,
         fee_reserve: Amount,
         expiry: u64,
-        request_lookup_id: String,
-        msat_to_pay: Option<Amount>,
+        request_lookup_id: PaymentIdentifier,
+        options: Option<MeltOptions>,
+        payment_method: PaymentMethod,
     ) -> Self {
         let id = Uuid::new_v4();
 
@@ -120,9 +286,10 @@ impl MeltQuote {
             expiry,
             payment_preimage: None,
             request_lookup_id,
-            msat_to_pay,
+            options,
             created_time: unix_time(),
             paid_time: None,
+            payment_method,
         }
     }
 }
@@ -173,16 +340,51 @@ impl From<MintQuote> for MintQuoteBolt11Response<Uuid> {
     fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<Uuid> {
         MintQuoteBolt11Response {
             quote: mint_quote.id,
+            state: mint_quote.state(),
             request: mint_quote.request,
-            state: mint_quote.state,
             expiry: Some(mint_quote.expiry),
             pubkey: mint_quote.pubkey,
-            amount: Some(mint_quote.amount),
+            amount: mint_quote.amount,
             unit: Some(mint_quote.unit.clone()),
         }
     }
 }
 
+impl From<MintQuote> for MintQuoteBolt11Response<String> {
+    fn from(quote: MintQuote) -> Self {
+        let quote: MintQuoteBolt11Response<Uuid> = quote.into();
+
+        quote.into()
+    }
+}
+
+impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<Uuid> {
+    type Error = crate::Error;
+
+    fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
+        Ok(MintQuoteBolt12Response {
+            quote: mint_quote.id,
+            request: mint_quote.request,
+            expiry: Some(mint_quote.expiry),
+            amount_paid: mint_quote.amount_paid,
+            amount_issued: mint_quote.amount_issued,
+            pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
+            amount: mint_quote.amount,
+            unit: mint_quote.unit,
+        })
+    }
+}
+
+impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
+    type Error = crate::Error;
+
+    fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
+        let quote: MintQuoteBolt12Response<Uuid> = quote.try_into()?;
+
+        Ok(quote.into())
+    }
+}
+
 impl From<&MeltQuote> for MeltQuoteBolt11Response<Uuid> {
     fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
         MeltQuoteBolt11Response {
@@ -212,8 +414,61 @@ impl From<MeltQuote> for MeltQuoteBolt11Response<Uuid> {
             expiry: melt_quote.expiry,
             payment_preimage: melt_quote.payment_preimage,
             change: None,
-            request: Some(melt_quote.request.clone()),
+            request: Some(melt_quote.request.to_string()),
             unit: Some(melt_quote.unit.clone()),
         }
     }
 }
+
+/// Payment request
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub enum MeltPaymentRequest {
+    /// Bolt11 Payment
+    Bolt11 {
+        /// Bolt11 invoice
+        bolt11: Bolt11Invoice,
+    },
+    /// Bolt12 Payment
+    Bolt12 {
+        /// Offer
+        #[serde(with = "offer_serde")]
+        offer: Box<Offer>,
+        /// Invoice
+        invoice: Option<Vec<u8>>,
+    },
+}
+
+impl std::fmt::Display for MeltPaymentRequest {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
+            MeltPaymentRequest::Bolt12 { offer, invoice: _ } => write!(f, "{offer}"),
+        }
+    }
+}
+
+mod offer_serde {
+    use std::str::FromStr;
+
+    use serde::{self, Deserialize, Deserializer, Serializer};
+
+    use super::Offer;
+
+    pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let s = offer.to_string();
+        serializer.serialize_str(&s)
+    }
+
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        Ok(Box::new(Offer::from_str(&s).map_err(|_| {
+            serde::de::Error::custom("Invalid Bolt12 Offer")
+        })?))
+    }
+}

+ 225 - 18
crates/cdk-common/src/payment.rs

@@ -1,17 +1,21 @@
 //! CDK Mint Lightning
 
+use std::convert::Infallible;
 use std::pin::Pin;
 
 use async_trait::async_trait;
-use cashu::MeltOptions;
+use cashu::util::hex;
+use cashu::{Bolt11Invoice, MeltOptions};
 use futures::Stream;
+use lightning::offers::offer::Offer;
 use lightning_invoice::ParseOrSemanticError;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use thiserror::Error;
 
-use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
-use crate::{mint, Amount};
+use crate::mint::MeltPaymentRequest;
+use crate::nuts::{CurrencyUnit, MeltQuoteState};
+use crate::Amount;
 
 /// CDK Lightning Error
 #[derive(Debug, Error)]
@@ -31,6 +35,9 @@ pub enum Error {
     /// Payment state is unknown
     #[error("Payment state is unknown")]
     UnknownPaymentState,
+    /// Amount mismatch
+    #[error("Amount is not what is expected")]
+    AmountMismatch,
     /// Lightning Error
     #[error(transparent)]
     Lightning(Box<dyn std::error::Error + Send + Sync>),
@@ -55,11 +62,186 @@ pub enum Error {
     /// NUT23 Error
     #[error(transparent)]
     NUT23(#[from] crate::nuts::nut23::Error),
+    /// Hex error
+    #[error("Hex error")]
+    Hex(#[from] hex::Error),
+    /// Invalid hash
+    #[error("Invalid hash")]
+    InvalidHash,
     /// Custom
     #[error("`{0}`")]
     Custom(String),
 }
 
+impl From<Infallible> for Error {
+    fn from(_: Infallible) -> Self {
+        unreachable!("Infallible cannot be constructed")
+    }
+}
+
+/// Payment identifier types
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
+#[serde(tag = "type", content = "value")]
+pub enum PaymentIdentifier {
+    /// Label identifier
+    Label(String),
+    /// Offer ID identifier
+    OfferId(String),
+    /// Payment hash identifier
+    PaymentHash([u8; 32]),
+    /// Bolt12 payment hash
+    Bolt12PaymentHash([u8; 32]),
+    /// Custom Payment ID
+    CustomId(String),
+}
+
+impl PaymentIdentifier {
+    /// Create new [`PaymentIdentifier`]
+    pub fn new(kind: &str, identifier: &str) -> Result<Self, Error> {
+        match kind.to_lowercase().as_str() {
+            "label" => Ok(Self::Label(identifier.to_string())),
+            "offer_id" => Ok(Self::OfferId(identifier.to_string())),
+            "payment_hash" => Ok(Self::PaymentHash(
+                hex::decode(identifier)?
+                    .try_into()
+                    .map_err(|_| Error::InvalidHash)?,
+            )),
+            "bolt12_payment_hash" => Ok(Self::Bolt12PaymentHash(
+                hex::decode(identifier)?
+                    .try_into()
+                    .map_err(|_| Error::InvalidHash)?,
+            )),
+            "custom" => Ok(Self::CustomId(identifier.to_string())),
+            _ => Err(Error::UnsupportedPaymentOption),
+        }
+    }
+
+    /// Payment id kind
+    pub fn kind(&self) -> String {
+        match self {
+            Self::Label(_) => "label".to_string(),
+            Self::OfferId(_) => "offer_id".to_string(),
+            Self::PaymentHash(_) => "payment_hash".to_string(),
+            Self::Bolt12PaymentHash(_) => "bolt12_payment_hash".to_string(),
+            Self::CustomId(_) => "custom".to_string(),
+        }
+    }
+}
+
+impl std::fmt::Display for PaymentIdentifier {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Label(l) => write!(f, "{l}"),
+            Self::OfferId(o) => write!(f, "{o}"),
+            Self::PaymentHash(h) => write!(f, "{}", hex::encode(h)),
+            Self::Bolt12PaymentHash(h) => write!(f, "{}", hex::encode(h)),
+            Self::CustomId(c) => write!(f, "{c}"),
+        }
+    }
+}
+
+/// Options for creating a BOLT11 incoming payment request
+#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
+pub struct Bolt11IncomingPaymentOptions {
+    /// Optional description for the payment request
+    pub description: Option<String>,
+    /// Amount for the payment request in sats
+    pub amount: Amount,
+    /// Optional expiry time as Unix timestamp in seconds
+    pub unix_expiry: Option<u64>,
+}
+
+/// Options for creating a BOLT12 incoming payment request
+#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
+pub struct Bolt12IncomingPaymentOptions {
+    /// Optional description for the payment request
+    pub description: Option<String>,
+    /// Optional amount for the payment request in sats
+    pub amount: Option<Amount>,
+    /// Optional expiry time as Unix timestamp in seconds
+    pub unix_expiry: Option<u64>,
+}
+
+/// Options for creating an incoming payment request
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum IncomingPaymentOptions {
+    /// BOLT11 payment request options
+    Bolt11(Bolt11IncomingPaymentOptions),
+    /// BOLT12 payment request options
+    Bolt12(Box<Bolt12IncomingPaymentOptions>),
+}
+
+/// Options for BOLT11 outgoing payments
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Bolt11OutgoingPaymentOptions {
+    /// Bolt11
+    pub bolt11: Bolt11Invoice,
+    /// Maximum fee amount allowed for the payment
+    pub max_fee_amount: Option<Amount>,
+    /// Optional timeout in seconds
+    pub timeout_secs: Option<u64>,
+    /// Melt options
+    pub melt_options: Option<MeltOptions>,
+}
+
+/// Options for BOLT12 outgoing payments
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Bolt12OutgoingPaymentOptions {
+    /// Offer
+    pub offer: Offer,
+    /// Maximum fee amount allowed for the payment
+    pub max_fee_amount: Option<Amount>,
+    /// Optional timeout in seconds
+    pub timeout_secs: Option<u64>,
+    /// Bolt12 Invoice
+    pub invoice: Option<Vec<u8>>,
+    /// Melt options
+    pub melt_options: Option<MeltOptions>,
+}
+
+/// Options for creating an outgoing payment
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum OutgoingPaymentOptions {
+    /// BOLT11 payment options
+    Bolt11(Box<Bolt11OutgoingPaymentOptions>),
+    /// BOLT12 payment options
+    Bolt12(Box<Bolt12OutgoingPaymentOptions>),
+}
+
+impl TryFrom<crate::mint::MeltQuote> for OutgoingPaymentOptions {
+    type Error = Error;
+
+    fn try_from(melt_quote: crate::mint::MeltQuote) -> Result<Self, Self::Error> {
+        match melt_quote.request {
+            MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
+                Bolt11OutgoingPaymentOptions {
+                    max_fee_amount: Some(melt_quote.fee_reserve),
+                    timeout_secs: None,
+                    bolt11,
+                    melt_options: melt_quote.options,
+                },
+            ))),
+            MeltPaymentRequest::Bolt12 { offer, invoice } => {
+                let melt_options = match melt_quote.options {
+                    None => None,
+                    Some(MeltOptions::Mpp { mpp: _ }) => return Err(Error::UnsupportedUnit),
+                    Some(options) => Some(options),
+                };
+
+                Ok(OutgoingPaymentOptions::Bolt12(Box::new(
+                    Bolt12OutgoingPaymentOptions {
+                        max_fee_amount: Some(melt_quote.fee_reserve),
+                        timeout_secs: None,
+                        offer: *offer,
+                        invoice,
+                        melt_options,
+                    },
+                )))
+            }
+        }
+    }
+}
+
 /// Mint payment trait
 #[async_trait]
 pub trait MintPayment {
@@ -72,34 +254,30 @@ pub trait MintPayment {
     /// Create a new invoice
     async fn create_incoming_payment_request(
         &self,
-        amount: Amount,
         unit: &CurrencyUnit,
-        description: String,
-        unix_expiry: Option<u64>,
+        options: IncomingPaymentOptions,
     ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
 
     /// Get payment quote
     /// Used to get fee and amount required for a payment request
     async fn get_payment_quote(
         &self,
-        request: &str,
         unit: &CurrencyUnit,
-        options: Option<MeltOptions>,
+        options: OutgoingPaymentOptions,
     ) -> Result<PaymentQuoteResponse, Self::Err>;
 
     /// Pay request
     async fn make_payment(
         &self,
-        melt_quote: mint::MeltQuote,
-        partial_amount: Option<Amount>,
-        max_fee_amount: Option<Amount>,
+        unit: &CurrencyUnit,
+        options: OutgoingPaymentOptions,
     ) -> 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_incoming_payment(
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err>;
+    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err>;
 
     /// Is wait invoice active
     fn is_wait_invoice_active(&self) -> bool;
@@ -110,21 +288,36 @@ pub trait MintPayment {
     /// Check the status of an incoming payment
     async fn check_incoming_payment_status(
         &self,
-        request_lookup_id: &str,
-    ) -> Result<MintQuoteState, Self::Err>;
+        payment_identifier: &PaymentIdentifier,
+    ) -> Result<Vec<WaitPaymentResponse>, Self::Err>;
 
     /// Check the status of an outgoing payment
     async fn check_outgoing_payment(
         &self,
-        request_lookup_id: &str,
+        payment_identifier: &PaymentIdentifier,
     ) -> Result<MakePaymentResponse, Self::Err>;
 }
 
+/// Wait any invoice response
+#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
+pub struct WaitPaymentResponse {
+    /// Request look up id
+    /// Id that relates the quote and payment request
+    pub payment_identifier: PaymentIdentifier,
+    /// Payment amount
+    pub payment_amount: Amount,
+    /// Unit
+    pub unit: CurrencyUnit,
+    /// Unique id of payment
+    // Payment hash
+    pub payment_id: String,
+}
+
 /// Create incoming payment response
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct CreateIncomingPaymentResponse {
     /// Id that is used to look up the payment from the ln backend
-    pub request_lookup_id: String,
+    pub request_lookup_id: PaymentIdentifier,
     /// Payment request
     pub request: String,
     /// Unix Expiry of Invoice
@@ -135,7 +328,7 @@ pub struct CreateIncomingPaymentResponse {
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MakePaymentResponse {
     /// Payment hash
-    pub payment_lookup_id: String,
+    pub payment_lookup_id: PaymentIdentifier,
     /// Payment proof
     pub payment_proof: Option<String>,
     /// Status
@@ -150,7 +343,7 @@ pub struct MakePaymentResponse {
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct PaymentQuoteResponse {
     /// Request look up id
-    pub request_lookup_id: String,
+    pub request_lookup_id: PaymentIdentifier,
     /// Amount
     pub amount: Amount,
     /// Fee required for melt
@@ -159,6 +352,18 @@ pub struct PaymentQuoteResponse {
     pub unit: CurrencyUnit,
     /// Status
     pub state: MeltQuoteState,
+    /// Payment Quote Options
+    pub options: Option<PaymentQuoteOptions>,
+}
+
+/// Payment quote options
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub enum PaymentQuoteOptions {
+    /// Bolt12 payment options
+    Bolt12 {
+        /// Bolt12 invoice
+        invoice: Option<Vec<u8>>,
+    },
 }
 
 /// Ln backend settings
@@ -172,6 +377,8 @@ pub struct Bolt11Settings {
     pub invoice_description: bool,
     /// Paying amountless invoices supported
     pub amountless: bool,
+    /// Bolt12 supported
+    pub bolt12: bool,
 }
 
 impl TryFrom<Bolt11Settings> for Value {

+ 3 - 0
crates/cdk-common/src/subscription.rs

@@ -81,6 +81,9 @@ impl Indexable for NotificationPayload<Uuid> {
             NotificationPayload::MintQuoteBolt11Response(mint_quote) => {
                 vec![Index::from(Notification::MintQuoteBolt11(mint_quote.quote))]
             }
+            NotificationPayload::MintQuoteBolt12Response(mint_quote) => {
+                vec![Index::from(Notification::MintQuoteBolt12(mint_quote.quote))]
+            }
         }
     }
 }

+ 67 - 2
crates/cdk-common/src/wallet.rs

@@ -6,7 +6,7 @@ use std::str::FromStr;
 
 use bitcoin::hashes::{sha256, Hash, HashEngine};
 use cashu::util::hex;
-use cashu::{nut00, Proofs, PublicKey};
+use cashu::{nut00, PaymentMethod, Proofs, PublicKey};
 use serde::{Deserialize, Serialize};
 
 use crate::mint_url::MintUrl;
@@ -42,8 +42,11 @@ pub struct MintQuote {
     pub id: String,
     /// Mint Url
     pub mint_url: MintUrl,
+    /// Payment method
+    #[serde(default)]
+    pub payment_method: PaymentMethod,
     /// Amount of quote
-    pub amount: Amount,
+    pub amount: Option<Amount>,
     /// Unit of quote
     pub unit: CurrencyUnit,
     /// Quote payment request e.g. bolt11
@@ -54,6 +57,12 @@ pub struct MintQuote {
     pub expiry: u64,
     /// Secretkey for signing mint quotes [NUT-20]
     pub secret_key: Option<SecretKey>,
+    /// Amount minted
+    #[serde(default)]
+    pub amount_issued: Amount,
+    /// Amount paid to the mint for the quote
+    #[serde(default)]
+    pub amount_paid: Amount,
 }
 
 /// Melt Quote Info
@@ -77,6 +86,62 @@ pub struct MeltQuote {
     pub payment_preimage: Option<String>,
 }
 
+impl MintQuote {
+    /// Create a new MintQuote
+    #[allow(clippy::too_many_arguments)]
+    pub fn new(
+        id: String,
+        mint_url: MintUrl,
+        payment_method: PaymentMethod,
+        amount: Option<Amount>,
+        unit: CurrencyUnit,
+        request: String,
+        expiry: u64,
+        secret_key: Option<SecretKey>,
+    ) -> Self {
+        Self {
+            id,
+            mint_url,
+            payment_method,
+            amount,
+            unit,
+            request,
+            state: MintQuoteState::Unpaid,
+            expiry,
+            secret_key,
+            amount_issued: Amount::ZERO,
+            amount_paid: Amount::ZERO,
+        }
+    }
+
+    /// Calculate the total amount including any fees
+    pub fn total_amount(&self) -> Amount {
+        self.amount_paid
+    }
+
+    /// Check if the quote has expired
+    pub fn is_expired(&self, current_time: u64) -> bool {
+        current_time > self.expiry
+    }
+
+    /// Amount that can be minted
+    pub fn amount_mintable(&self) -> Amount {
+        if self.amount_issued > self.amount_paid {
+            return Amount::ZERO;
+        }
+
+        let difference = self.amount_paid - self.amount_issued;
+
+        if difference == Amount::ZERO && self.state != MintQuoteState::Issued {
+            if let Some(amount) = self.amount {
+                return amount;
+            }
+        }
+
+        difference
+    }
+}
+
 /// Send Kind
 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
 pub enum SendKind {

+ 3 - 0
crates/cdk-common/src/ws.rs

@@ -60,6 +60,9 @@ pub fn notification_uuid_to_notification_string(
             NotificationPayload::MintQuoteBolt11Response(quote) => {
                 NotificationPayload::MintQuoteBolt11Response(quote.to_string_id())
             }
+            NotificationPayload::MintQuoteBolt12Response(quote) => {
+                NotificationPayload::MintQuoteBolt12Response(quote.to_string_id())
+            }
         },
     }
 }

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

@@ -13,7 +13,7 @@ readme = "README.md"
 [dependencies]
 async-trait.workspace = true
 bitcoin.workspace = true
-cdk = { workspace = true, features = ["mint"] }
+cdk-common = { workspace = true, features = ["mint"] }
 futures.workspace = true
 tokio.workspace = true
 tokio-util.workspace = true
@@ -22,5 +22,6 @@ thiserror.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 lightning-invoice.workspace = true
+lightning.workspace = true
 tokio-stream.workspace = true
 reqwest.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_payment::Error {
+impl From<Error> for cdk_common::payment::Error {
     fn from(e: Error) -> Self {
         Self::Lightning(Box::new(e))
     }

+ 260 - 112
crates/cdk-fake-wallet/src/lib.rs

@@ -9,7 +9,6 @@
 use std::cmp::max;
 use std::collections::{HashMap, HashSet};
 use std::pin::Pin;
-use std::str::FromStr;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 
@@ -17,22 +16,23 @@ 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::{to_unit, Amount};
-use cdk::cdk_payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
-    PaymentQuoteResponse,
+use cdk_common::amount::{to_unit, Amount};
+use cdk_common::common::FeeReserve;
+use cdk_common::ensure_cdk;
+use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
+use cdk_common::payment::{
+    self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
+    MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
+    PaymentQuoteResponse, WaitPaymentResponse,
 };
-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::offers::offer::OfferBuilder;
 use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
-use reqwest::Client;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use tokio::sync::Mutex;
+use tokio::sync::{Mutex, RwLock};
 use tokio::time;
 use tokio_stream::wrappers::ReceiverStream;
 use tokio_util::sync::CancellationToken;
@@ -44,13 +44,16 @@ pub mod error;
 #[derive(Clone)]
 pub struct FakeWallet {
     fee_reserve: FeeReserve,
-    sender: tokio::sync::mpsc::Sender<String>,
-    receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
+    #[allow(clippy::type_complexity)]
+    sender: tokio::sync::mpsc::Sender<(PaymentIdentifier, Amount)>,
+    #[allow(clippy::type_complexity)]
+    receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<(PaymentIdentifier, Amount)>>>>,
     payment_states: Arc<Mutex<HashMap<String, MeltQuoteState>>>,
     failed_payment_check: Arc<Mutex<HashSet<String>>>,
     payment_delay: u64,
     wait_invoice_cancel_token: CancellationToken,
     wait_invoice_is_active: Arc<AtomicBool>,
+    incoming_payments: Arc<RwLock<HashMap<PaymentIdentifier, Vec<WaitPaymentResponse>>>>,
 }
 
 impl FakeWallet {
@@ -72,6 +75,7 @@ impl FakeWallet {
             payment_delay,
             wait_invoice_cancel_token: CancellationToken::new(),
             wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
+            incoming_payments: Arc::new(RwLock::new(HashMap::new())),
         }
     }
 }
@@ -102,7 +106,7 @@ impl Default for FakeInvoiceDescription {
 
 #[async_trait]
 impl MintPayment for FakeWallet {
-    type Err = cdk_payment::Error;
+    type Err = payment::Error;
 
     #[instrument(skip_all)]
     async fn get_settings(&self) -> Result<Value, Self::Err> {
@@ -111,6 +115,7 @@ impl MintPayment for FakeWallet {
             unit: CurrencyUnit::Msat,
             invoice_description: true,
             amountless: false,
+            bolt12: false,
         })?)
     }
 
@@ -127,51 +132,86 @@ impl MintPayment for FakeWallet {
     #[instrument(skip_all)]
     async fn wait_any_incoming_payment(
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
+    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
         tracing::info!("Starting stream for fake invoices");
-        let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?;
+        let receiver = self
+            .receiver
+            .lock()
+            .await
+            .take()
+            .ok_or(Error::NoReceiver)
+            .unwrap();
         let receiver_stream = ReceiverStream::new(receiver);
-        Ok(Box::pin(receiver_stream.map(|label| label)))
+        Ok(Box::pin(receiver_stream.map(
+            |(request_lookup_id, payment_amount)| WaitPaymentResponse {
+                payment_identifier: request_lookup_id.clone(),
+                payment_amount,
+                unit: CurrencyUnit::Sat,
+                payment_id: request_lookup_id.to_string(),
+            },
+        )))
     }
 
     #[instrument(skip_all)]
     async fn get_payment_quote(
         &self,
-        request: &str,
         unit: &CurrencyUnit,
-        options: Option<MeltOptions>,
+        options: OutgoingPaymentOptions,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        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_msat, request_lookup_id) = match options {
+            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
+                // If we have specific amount options, use those
+                let amount_msat: u64 = if let Some(melt_options) = bolt11_options.melt_options {
+                    let msats = match melt_options {
+                        MeltOptions::Amountless { amountless } => {
+                            let amount_msat = amountless.amount_msat;
+
+                            if let Some(invoice_amount) =
+                                bolt11_options.bolt11.amount_milli_satoshis()
+                            {
+                                ensure_cdk!(
+                                    invoice_amount == u64::from(amount_msat),
+                                    Error::UnknownInvoiceAmount.into()
+                                );
+                            }
+                            amount_msat
+                        }
+                        MeltOptions::Mpp { mpp } => mpp.amount,
+                    };
+
+                    u64::from(msats)
+                } else {
+                    // Fall back to invoice amount
+                    bolt11_options
+                        .bolt11
+                        .amount_milli_satoshis()
+                        .ok_or(Error::UnknownInvoiceAmount)?
+                };
+                let payment_id =
+                    PaymentIdentifier::PaymentHash(*bolt11_options.bolt11.payment_hash().as_ref());
+                (amount_msat, payment_id)
+            }
+            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
+                let offer = bolt12_options.offer;
+
+                let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
+                    amount.amount_msat().into()
+                } else {
+                    // Fall back to offer amount
+                    let amount = offer.amount().ok_or(Error::UnknownInvoiceAmount)?;
+                    match amount {
+                        lightning::offers::offer::Amount::Bitcoin { amount_msats } => amount_msats,
+                        _ => return Err(Error::UnknownInvoiceAmount.into()),
+                    }
+                };
+                (
+                    amount_msat,
+                    PaymentIdentifier::OfferId(offer.id().to_string()),
+                )
+            }
         };
 
-        let amount = if unit != &CurrencyUnit::Sat && unit != &CurrencyUnit::Msat {
-            let client = Client::new();
-
-            let response: Value = client
-                .get("https://mempool.space/api/v1/prices")
-                .send()
-                .await
-                .map_err(|_| Error::UnknownInvoice)?
-                .json()
-                .await
-                .unwrap();
-
-            let price = response.get(unit.to_string().to_uppercase()).unwrap();
-
-            let bitcoin_amount = u64::from(amount_msat) as f64 / 100_000_000_000.0;
-            let total_price = price.as_f64().unwrap() * bitcoin_amount;
-
-            Amount::from((total_price * 100.0).ceil() as u64)
-        } else {
-            to_unit(amount_msat, &CurrencyUnit::Msat, unit)?
-        };
+        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;
@@ -181,96 +221,198 @@ impl MintPayment for FakeWallet {
         let fee = max(relative_fee_reserve, absolute_fee_reserve);
 
         Ok(PaymentQuoteResponse {
-            request_lookup_id: bolt11.payment_hash().to_string(),
+            request_lookup_id,
             amount,
             fee: fee.into(),
-            unit: unit.clone(),
             state: MeltQuoteState::Unpaid,
+            options: None,
+            unit: unit.clone(),
         })
     }
 
     #[instrument(skip_all)]
     async fn make_payment(
         &self,
-        melt_quote: mint::MeltQuote,
-        _partial_msats: Option<Amount>,
-        _max_fee_msats: Option<Amount>,
+        unit: &CurrencyUnit,
+        options: OutgoingPaymentOptions,
     ) -> Result<MakePaymentResponse, Self::Err> {
-        let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
-
-        let payment_hash = bolt11.payment_hash().to_string();
-
-        let description = bolt11.description().to_string();
-
-        let status: Option<FakeInvoiceDescription> = serde_json::from_str(&description).ok();
-
-        let mut payment_states = self.payment_states.lock().await;
-        let payment_status = status
-            .clone()
-            .map(|s| s.pay_invoice_state)
-            .unwrap_or(MeltQuoteState::Paid);
-
-        let checkout_going_status = status
-            .clone()
-            .map(|s| s.check_payment_state)
-            .unwrap_or(MeltQuoteState::Paid);
-
-        payment_states.insert(payment_hash.clone(), checkout_going_status);
-
-        if let Some(description) = status {
-            if description.check_err {
-                let mut fail = self.failed_payment_check.lock().await;
-                fail.insert(payment_hash.clone());
+        match options {
+            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
+                let bolt11 = bolt11_options.bolt11;
+                let payment_hash = bolt11.payment_hash().to_string();
+
+                let description = bolt11.description().to_string();
+
+                let status: Option<FakeInvoiceDescription> =
+                    serde_json::from_str(&description).ok();
+
+                let mut payment_states = self.payment_states.lock().await;
+                let payment_status = status
+                    .clone()
+                    .map(|s| s.pay_invoice_state)
+                    .unwrap_or(MeltQuoteState::Paid);
+
+                let checkout_going_status = status
+                    .clone()
+                    .map(|s| s.check_payment_state)
+                    .unwrap_or(MeltQuoteState::Paid);
+
+                payment_states.insert(payment_hash.clone(), checkout_going_status);
+
+                if let Some(description) = status {
+                    if description.check_err {
+                        let mut fail = self.failed_payment_check.lock().await;
+                        fail.insert(payment_hash.clone());
+                    }
+
+                    ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into());
+                }
+
+                let amount_msat: u64 = if let Some(melt_options) = bolt11_options.melt_options {
+                    melt_options.amount_msat().into()
+                } else {
+                    // Fall back to invoice amount
+                    bolt11
+                        .amount_milli_satoshis()
+                        .ok_or(Error::UnknownInvoiceAmount)?
+                };
+
+                let total_spent = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+
+                Ok(MakePaymentResponse {
+                    payment_proof: Some("".to_string()),
+                    payment_lookup_id: PaymentIdentifier::PaymentHash(
+                        *bolt11.payment_hash().as_ref(),
+                    ),
+                    status: payment_status,
+                    total_spent: total_spent + 1.into(),
+                    unit: unit.clone(),
+                })
+            }
+            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
+                let bolt12 = bolt12_options.offer;
+                let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
+                    amount.amount_msat().into()
+                } else {
+                    // Fall back to offer amount
+                    let amount = bolt12.amount().ok_or(Error::UnknownInvoiceAmount)?;
+                    match amount {
+                        lightning::offers::offer::Amount::Bitcoin { amount_msats } => amount_msats,
+                        _ => return Err(Error::UnknownInvoiceAmount.into()),
+                    }
+                };
+
+                let total_spent = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+
+                Ok(MakePaymentResponse {
+                    payment_proof: Some("".to_string()),
+                    payment_lookup_id: PaymentIdentifier::OfferId(bolt12.id().to_string()),
+                    status: MeltQuoteState::Paid,
+                    total_spent: total_spent + 1.into(),
+                    unit: unit.clone(),
+                })
             }
-
-            ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into());
         }
-
-        Ok(MakePaymentResponse {
-            payment_proof: Some("".to_string()),
-            payment_lookup_id: payment_hash,
-            status: payment_status,
-            total_spent: melt_quote.amount + 1.into(),
-            unit: melt_quote.unit,
-        })
     }
 
     #[instrument(skip_all)]
     async fn create_incoming_payment_request(
         &self,
-        amount: Amount,
-        _unit: &CurrencyUnit,
-        description: String,
-        _unix_expiry: Option<u64>,
+        unit: &CurrencyUnit,
+        options: IncomingPaymentOptions,
     ) -> 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;
-
-        let invoice = create_fake_invoice(amount_msat.into(), description);
+        let (payment_hash, request, amount, expiry) = match options {
+            IncomingPaymentOptions::Bolt12(bolt12_options) => {
+                let description = bolt12_options.description.unwrap_or_default();
+                let amount = bolt12_options.amount;
+                let expiry = bolt12_options.unix_expiry;
+
+                let secret_key = SecretKey::new(&mut thread_rng());
+                let secp_ctx = Secp256k1::new();
+
+                let offer_builder = OfferBuilder::new(secret_key.public_key(&secp_ctx))
+                    .description(description.clone());
+
+                let offer_builder = match amount {
+                    Some(amount) => {
+                        let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
+                        offer_builder.amount_msats(amount_msat.into())
+                    }
+                    None => offer_builder,
+                };
+
+                let offer = offer_builder.build().unwrap();
+
+                (
+                    PaymentIdentifier::OfferId(offer.id().to_string()),
+                    offer.to_string(),
+                    amount.unwrap_or(Amount::ZERO),
+                    expiry,
+                )
+            }
+            IncomingPaymentOptions::Bolt11(bolt11_options) => {
+                let description = bolt11_options.description.unwrap_or_default();
+                let amount = bolt11_options.amount;
+                let expiry = bolt11_options.unix_expiry;
+
+                // Since this is fake we just use the amount no matter the unit to create an invoice
+                let invoice = create_fake_invoice(amount.into(), description.clone());
+                let payment_hash = invoice.payment_hash();
+
+                (
+                    PaymentIdentifier::PaymentHash(*payment_hash.as_ref()),
+                    invoice.to_string(),
+                    amount,
+                    expiry,
+                )
+            }
+        };
 
         let sender = self.sender.clone();
+        let duration = time::Duration::from_secs(self.payment_delay);
 
-        let payment_hash = invoice.payment_hash();
+        let final_amount = if amount == Amount::ZERO {
+            let mut rng = thread_rng();
+            // Generate a random number between 1 and 1000 (inclusive)
+            let random_number: u64 = rng.gen_range(1..=1000);
+            random_number.into()
+        } else {
+            amount
+        };
 
-        let payment_hash_clone = payment_hash.to_string();
+        let payment_hash_clone = payment_hash.clone();
 
-        let duration = time::Duration::from_secs(self.payment_delay);
+        let incoming_payment = self.incoming_payments.clone();
 
         tokio::spawn(async move {
             // Wait for the random delay to elapse
             time::sleep(duration).await;
 
+            let response = WaitPaymentResponse {
+                payment_identifier: payment_hash_clone.clone(),
+                payment_amount: final_amount,
+                unit: CurrencyUnit::Sat,
+                payment_id: payment_hash_clone.to_string(),
+            };
+            let mut incoming = incoming_payment.write().await;
+            incoming
+                .entry(payment_hash_clone.clone())
+                .or_insert_with(Vec::new)
+                .push(response.clone());
+
             // Send the message after waiting for the specified duration
-            if sender.send(payment_hash_clone.clone()).await.is_err() {
-                tracing::error!("Failed to send label: {}", payment_hash_clone);
+            if sender
+                .send((payment_hash_clone.clone(), final_amount))
+                .await
+                .is_err()
+            {
+                tracing::error!("Failed to send label: {:?}", payment_hash_clone);
             }
         });
 
-        let expiry = invoice.expires_at().map(|t| t.as_secs());
-
         Ok(CreateIncomingPaymentResponse {
-            request_lookup_id: payment_hash.to_string(),
-            request: invoice.to_string(),
+            request_lookup_id: payment_hash,
+            request,
             expiry,
         })
     }
@@ -278,31 +420,37 @@ impl MintPayment for FakeWallet {
     #[instrument(skip_all)]
     async fn check_incoming_payment_status(
         &self,
-        _request_lookup_id: &str,
-    ) -> Result<MintQuoteState, Self::Err> {
-        Ok(MintQuoteState::Paid)
+        request_lookup_id: &PaymentIdentifier,
+    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
+        Ok(self
+            .incoming_payments
+            .read()
+            .await
+            .get(request_lookup_id)
+            .cloned()
+            .unwrap_or(vec![]))
     }
 
     #[instrument(skip_all)]
     async fn check_outgoing_payment(
         &self,
-        request_lookup_id: &str,
+        request_lookup_id: &PaymentIdentifier,
     ) -> 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();
+        let status = states.get(&request_lookup_id.to_string()).cloned();
 
         let status = status.unwrap_or(MeltQuoteState::Paid);
 
         let fail_payments = self.failed_payment_check.lock().await;
 
-        if fail_payments.contains(request_lookup_id) {
-            return Err(cdk_payment::Error::InvoicePaymentPending);
+        if fail_payments.contains(&request_lookup_id.to_string()) {
+            return Err(payment::Error::InvoicePaymentPending);
         }
 
         Ok(MakePaymentResponse {
             payment_proof: Some("".to_string()),
-            payment_lookup_id: request_lookup_id.to_string(),
+            payment_lookup_id: request_lookup_id.clone(),
             status,
             total_spent: Amount::ZERO,
             unit: CurrencyUnit::Msat,

+ 57 - 3
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -8,6 +8,7 @@ use std::{env, fs};
 use anyhow::{anyhow, bail, Result};
 use async_trait::async_trait;
 use bip39::Mnemonic;
+use cashu::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
 use cdk::amount::SplitTarget;
 use cdk::cdk_database::{self, WalletDatabase};
 use cdk::mint::{MintBuilder, MintMeltLimits};
@@ -72,7 +73,7 @@ impl MintConnector for DirectMintConnection {
         request: MintQuoteBolt11Request,
     ) -> Result<MintQuoteBolt11Response<String>, Error> {
         self.mint
-            .get_mint_bolt11_quote(request)
+            .get_mint_quote(request.into())
             .await
             .map(Into::into)
     }
@@ -98,7 +99,7 @@ impl MintConnector for DirectMintConnection {
         request: MeltQuoteBolt11Request,
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
         self.mint
-            .get_melt_bolt11_quote(&request)
+            .get_melt_quote(request.into())
             .await
             .map(Into::into)
     }
@@ -119,7 +120,7 @@ impl MintConnector for DirectMintConnection {
         request: MeltRequest<String>,
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
         let request_uuid = request.try_into().unwrap();
-        self.mint.melt_bolt11(&request_uuid).await.map(Into::into)
+        self.mint.melt(&request_uuid).await.map(Into::into)
     }
 
     async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
@@ -152,6 +153,59 @@ impl MintConnector for DirectMintConnection {
 
         *auth_wallet = wallet;
     }
+
+    async fn post_mint_bolt12_quote(
+        &self,
+        request: MintQuoteBolt12Request,
+    ) -> Result<MintQuoteBolt12Response<String>, Error> {
+        let res: MintQuoteBolt12Response<Uuid> =
+            self.mint.get_mint_quote(request.into()).await?.try_into()?;
+        Ok(res.into())
+    }
+
+    async fn get_mint_quote_bolt12_status(
+        &self,
+        quote_id: &str,
+    ) -> Result<MintQuoteBolt12Response<String>, Error> {
+        let quote_id_uuid = Uuid::from_str(quote_id).unwrap();
+        let quote: MintQuoteBolt12Response<Uuid> = self
+            .mint
+            .check_mint_quote(&quote_id_uuid)
+            .await?
+            .try_into()?;
+
+        Ok(quote.into())
+    }
+
+    /// Melt Quote [NUT-23]
+    async fn post_melt_bolt12_quote(
+        &self,
+        request: MeltQuoteBolt12Request,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        self.mint
+            .get_melt_quote(request.into())
+            .await
+            .map(Into::into)
+    }
+    /// Melt Quote Status [NUT-23]
+    async fn get_melt_bolt12_quote_status(
+        &self,
+        quote_id: &str,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        let quote_id_uuid = Uuid::from_str(quote_id).unwrap();
+        self.mint
+            .check_melt_quote(&quote_id_uuid)
+            .await
+            .map(Into::into)
+    }
+    /// Melt [NUT-23]
+    async fn post_melt_bolt12(
+        &self,
+        _request: MeltRequest<String>,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        // Implementation to be added later
+        Err(Error::UnsupportedPaymentMethod)
+    }
 }
 
 pub fn setup_tracing() {

+ 35 - 10
crates/cdk-integration-tests/src/lib.rs

@@ -2,7 +2,7 @@ use std::env;
 use std::sync::Arc;
 
 use anyhow::{anyhow, bail, Result};
-use cashu::Bolt11Invoice;
+use cashu::{Bolt11Invoice, PaymentMethod};
 use cdk::amount::{Amount, SplitTarget};
 use cdk::nuts::{MintQuoteState, NotificationPayload, State};
 use cdk::wallet::WalletSubscription;
@@ -86,6 +86,10 @@ pub async fn wait_for_mint_to_be_paid(
                 if response.state == MintQuoteState::Paid {
                     return Ok(());
                 }
+            } else if let NotificationPayload::MintQuoteBolt12Response(response) = msg {
+                if response.amount_paid > Amount::ZERO {
+                    return Ok(());
+                }
             }
         }
         Err(anyhow!("Subscription ended without quote being paid"))
@@ -95,18 +99,40 @@ pub async fn wait_for_mint_to_be_paid(
 
     let check_interval = Duration::from_secs(5);
 
+    let method = wallet
+        .localstore
+        .get_mint_quote(mint_quote_id)
+        .await?
+        .map(|q| q.payment_method)
+        .unwrap_or_default();
+
     let periodic_task = async {
         loop {
-            match wallet.mint_quote_state(mint_quote_id).await {
-                Ok(result) => {
-                    if result.state == MintQuoteState::Paid {
-                        tracing::info!("mint quote paid via poll");
-                        return Ok(());
+            match method {
+                PaymentMethod::Bolt11 => match wallet.mint_quote_state(mint_quote_id).await {
+                    Ok(result) => {
+                        if result.state == MintQuoteState::Paid {
+                            tracing::info!("mint quote paid via poll");
+                            return Ok(());
+                        }
+                    }
+                    Err(e) => {
+                        tracing::error!("Could not check mint quote status: {:?}", e);
+                    }
+                },
+                PaymentMethod::Bolt12 => {
+                    match wallet.mint_bolt12_quote_state(mint_quote_id).await {
+                        Ok(result) => {
+                            if result.amount_paid > Amount::ZERO {
+                                return Ok(());
+                            }
+                        }
+                        Err(e) => {
+                            tracing::error!("Could not check mint quote status: {:?}", e);
+                        }
                     }
                 }
-                Err(e) => {
-                    tracing::error!("Could not check mint quote status: {:?}", e);
-                }
+                PaymentMethod::Custom(_) => (),
             }
             sleep(check_interval).await;
         }
@@ -166,7 +192,6 @@ pub async fn init_lnd_client() -> LndClient {
 pub async fn pay_if_regtest(invoice: &Bolt11Invoice) -> Result<()> {
     // Check if the invoice is for the regtest network
     if invoice.network() == bitcoin::Network::Regtest {
-        println!("Regtest invoice");
         let lnd_client = init_lnd_client().await;
         lnd_client.pay_invoice(invoice.to_string()).await?;
         Ok(())

+ 332 - 0
crates/cdk-integration-tests/tests/bolt12.rs

@@ -0,0 +1,332 @@
+use std::sync::Arc;
+
+use anyhow::{bail, Result};
+use bip39::Mnemonic;
+use cashu::amount::SplitTarget;
+use cashu::nut23::Amountless;
+use cashu::{Amount, CurrencyUnit, MintRequest, PreMintSecrets, ProofsMethods};
+use cdk::wallet::{HttpClient, MintConnector, Wallet};
+use cdk_integration_tests::init_regtest::get_cln_dir;
+use cdk_integration_tests::{get_mint_url_from_env, wait_for_mint_to_be_paid};
+use cdk_sqlite::wallet::memory;
+use ln_regtest_rs::ln_client::ClnClient;
+
+/// Tests basic BOLT12 minting functionality:
+/// - Creates a wallet
+/// - Gets a BOLT12 quote for a specific amount (100 sats)
+/// - Pays the quote using Core Lightning
+/// - Mints tokens and verifies the correct amount is received
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_regtest_bolt12_mint() {
+    let wallet = Wallet::new(
+        &get_mint_url_from_env(),
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await.unwrap()),
+        &Mnemonic::generate(12).unwrap().to_seed_normalized(""),
+        None,
+    )
+    .unwrap();
+
+    let mint_amount = Amount::from(100);
+
+    let mint_quote = wallet
+        .mint_bolt12_quote(Some(mint_amount), None)
+        .await
+        .unwrap();
+
+    assert_eq!(mint_quote.amount, Some(mint_amount));
+
+    let cln_one_dir = get_cln_dir("one");
+    let cln_client = ClnClient::new(cln_one_dir.clone(), None).await.unwrap();
+    cln_client
+        .pay_bolt12_offer(None, mint_quote.request)
+        .await
+        .unwrap();
+
+    let proofs = wallet
+        .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
+        .await
+        .unwrap();
+
+    assert_eq!(proofs.total_amount().unwrap(), 100.into());
+}
+
+/// Tests multiple payments to a single BOLT12 quote:
+/// - Creates a wallet and gets a BOLT12 quote without specifying amount
+/// - Makes two separate payments (10,000 sats and 11,000 sats) to the same quote
+/// - Verifies that each payment can be minted separately and correctly
+/// - Tests the functionality of reusing a quote for multiple payments
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
+    let wallet = Wallet::new(
+        &get_mint_url_from_env(),
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
+
+    let cln_one_dir = get_cln_dir("one");
+    let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
+    cln_client
+        .pay_bolt12_offer(Some(10000), mint_quote.request.clone())
+        .await
+        .unwrap();
+
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+
+    wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
+
+    let proofs = wallet
+        .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
+        .await
+        .unwrap();
+
+    assert_eq!(proofs.total_amount().unwrap(), 10.into());
+
+    cln_client
+        .pay_bolt12_offer(Some(11_000), mint_quote.request)
+        .await
+        .unwrap();
+
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+
+    wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
+
+    let proofs = wallet
+        .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
+        .await
+        .unwrap();
+
+    assert_eq!(proofs.total_amount().unwrap(), 11.into());
+
+    Ok(())
+}
+
+/// Tests that multiple wallets can pay the same BOLT12 offer:
+/// - Creates a BOLT12 offer through CLN that both wallets will pay
+/// - Creates two separate wallets with different minting amounts
+/// - Has each wallet get their own quote and make payments
+/// - Verifies both wallets can successfully mint their tokens
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
+    // Create first wallet
+    let wallet_one = Wallet::new(
+        &get_mint_url_from_env(),
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    // Create second wallet
+    let wallet_two = Wallet::new(
+        &get_mint_url_from_env(),
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    // Create a BOLT12 offer that both wallets will use
+    let cln_one_dir = get_cln_dir("one");
+    let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
+    // First wallet payment
+    let quote_one = wallet_one
+        .mint_bolt12_quote(Some(10_000.into()), None)
+        .await?;
+    cln_client
+        .pay_bolt12_offer(None, quote_one.request.clone())
+        .await?;
+    wait_for_mint_to_be_paid(&wallet_one, &quote_one.id, 60).await?;
+    let proofs_one = wallet_one
+        .mint_bolt12(&quote_one.id, None, SplitTarget::default(), None)
+        .await?;
+
+    assert_eq!(proofs_one.total_amount()?, 10_000.into());
+
+    // Second wallet payment
+    let quote_two = wallet_two
+        .mint_bolt12_quote(Some(15_000.into()), None)
+        .await?;
+    cln_client
+        .pay_bolt12_offer(None, quote_two.request.clone())
+        .await?;
+    wait_for_mint_to_be_paid(&wallet_two, &quote_two.id, 60).await?;
+
+    let proofs_two = wallet_two
+        .mint_bolt12(&quote_two.id, None, SplitTarget::default(), None)
+        .await?;
+    assert_eq!(proofs_two.total_amount()?, 15_000.into());
+
+    let offer = cln_client
+        .get_bolt12_offer(None, false, "test_multiple_wallets".to_string())
+        .await?;
+
+    let wallet_one_melt_quote = wallet_one
+        .melt_bolt12_quote(
+            offer.to_string(),
+            Some(cashu::MeltOptions::Amountless {
+                amountless: Amountless {
+                    amount_msat: 1500.into(),
+                },
+            }),
+        )
+        .await?;
+
+    let wallet_two_melt_quote = wallet_two
+        .melt_bolt12_quote(
+            offer.to_string(),
+            Some(cashu::MeltOptions::Amountless {
+                amountless: Amountless {
+                    amount_msat: 1000.into(),
+                },
+            }),
+        )
+        .await?;
+
+    let melted = wallet_one.melt(&wallet_one_melt_quote.id).await?;
+
+    assert!(melted.preimage.is_some());
+
+    let melted_two = wallet_two.melt(&wallet_two_melt_quote.id).await?;
+
+    assert!(melted_two.preimage.is_some());
+
+    Ok(())
+}
+
+/// Tests the BOLT12 melting (spending) functionality:
+/// - Creates a wallet and mints 20,000 sats using BOLT12
+/// - Creates a BOLT12 offer for 10,000 sats
+/// - Tests melting (spending) tokens using the BOLT12 offer
+/// - Verifies the correct amount is melted
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_regtest_bolt12_melt() -> Result<()> {
+    let wallet = Wallet::new(
+        &get_mint_url_from_env(),
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    wallet.get_mint_info().await?;
+
+    let mint_amount = Amount::from(20_000);
+
+    // Create a single-use BOLT12 quote
+    let mint_quote = wallet.mint_bolt12_quote(Some(mint_amount), None).await?;
+
+    assert_eq!(mint_quote.amount, Some(mint_amount));
+    // Pay the quote
+    let cln_one_dir = get_cln_dir("one");
+    let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
+    cln_client
+        .pay_bolt12_offer(None, mint_quote.request.clone())
+        .await?;
+
+    // Wait for payment to be processed
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+
+    let offer = cln_client
+        .get_bolt12_offer(Some(10_000), true, "hhhhhhhh".to_string())
+        .await?;
+
+    let _proofs = wallet
+        .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
+        .await
+        .unwrap();
+
+    let quote = wallet.melt_bolt12_quote(offer.to_string(), None).await?;
+
+    let melt = wallet.melt(&quote.id).await?;
+
+    assert_eq!(melt.amount, 10.into());
+
+    Ok(())
+}
+
+/// Tests security validation for BOLT12 minting to prevent overspending:
+/// - Creates a wallet and gets an open-ended BOLT12 quote
+/// - Makes a payment of 10,000 millisats
+/// - Attempts to mint more tokens (500 sats) than were actually paid for
+/// - Verifies that the mint correctly rejects the oversized mint request
+/// - Ensures proper error handling with TransactionUnbalanced error
+/// This test is crucial for ensuring the economic security of the minting process
+/// by preventing users from minting more tokens than they have paid for.
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_regtest_bolt12_mint_extra() -> Result<()> {
+    let wallet = Wallet::new(
+        &get_mint_url_from_env(),
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    wallet.get_mint_info().await?;
+
+    // Create a single-use BOLT12 quote
+    let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
+
+    let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
+
+    assert_eq!(state.amount_paid, Amount::ZERO);
+    assert_eq!(state.amount_issued, Amount::ZERO);
+
+    let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
+
+    let pay_amount_msats = 10_000;
+
+    let cln_one_dir = get_cln_dir("one");
+    let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
+    cln_client
+        .pay_bolt12_offer(Some(pay_amount_msats), mint_quote.request.clone())
+        .await?;
+
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10).await?;
+
+    let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
+
+    assert_eq!(state.amount_paid, (pay_amount_msats / 1_000).into());
+    assert_eq!(state.amount_issued, Amount::ZERO);
+
+    let pre_mint = PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None)?;
+
+    let quote_info = wallet
+        .localstore
+        .get_mint_quote(&mint_quote.id)
+        .await?
+        .expect("there is a quote");
+
+    let mut mint_request = MintRequest {
+        quote: mint_quote.id,
+        outputs: pre_mint.blinded_messages(),
+        signature: None,
+    };
+
+    if let Some(secret_key) = quote_info.secret_key {
+        mint_request.sign(secret_key)?;
+    }
+
+    let http_client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
+
+    let response = http_client.post_mint(mint_request.clone()).await;
+
+    match response {
+        Err(err) => match err {
+            cdk::Error::TransactionUnbalanced(_, _, _) => (),
+            err => {
+                bail!("Wrong mint error returned: {}", err.to_string());
+            }
+        },
+        Ok(_) => {
+            bail!("Should not have allowed second payment");
+        }
+    }
+
+    Ok(())
+}

+ 3 - 3
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -514,7 +514,7 @@ async fn test_reuse_auth_proof() {
             .await
             .expect("Quote should be allowed");
 
-        assert!(quote.amount == 10.into());
+        assert!(quote.amount == Some(10.into()));
     }
 
     wallet
@@ -645,7 +645,7 @@ async fn test_refresh_access_token() {
         .await
         .expect("failed to get mint quote with refreshed token");
 
-    assert_eq!(mint_quote.amount, mint_amount);
+    assert_eq!(mint_quote.amount, Some(mint_amount));
 
     // Verify the total number of auth tokens
     let total_auth_proofs = wallet.get_unspent_auth_proofs().await.unwrap();
@@ -731,7 +731,7 @@ async fn test_auth_token_spending_order() {
             .await
             .expect("failed to get mint quote");
 
-        assert_eq!(mint_quote.amount, 10.into());
+        assert_eq!(mint_quote.amount, Some(10.into()));
 
         // Check remaining tokens after each operation
         let remaining = wallet.get_unspent_auth_proofs().await.unwrap();

+ 10 - 2
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -100,6 +100,10 @@ async fn test_happy_mint_melt_round_trip() {
     let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
     pay_if_regtest(&invoice).await.unwrap();
 
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10)
+        .await
+        .unwrap();
+
     let proofs = wallet
         .mint(&mint_quote.id, SplitTarget::default(), None)
         .await
@@ -210,7 +214,7 @@ async fn test_happy_mint() {
 
     let mint_quote = wallet.mint_quote(mint_amount, None).await.unwrap();
 
-    assert_eq!(mint_quote.amount, mint_amount);
+    assert_eq!(mint_quote.amount, Some(mint_amount));
 
     let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
     pay_if_regtest(&invoice).await.unwrap();
@@ -285,6 +289,8 @@ async fn test_restore() {
     let restored = wallet_2.restore().await.unwrap();
     let proofs = wallet_2.get_unspent_proofs().await.unwrap();
 
+    assert!(!proofs.is_empty());
+
     let expected_fee = wallet.get_proofs_fee(&proofs).await.unwrap();
     wallet_2
         .swap(None, SplitTarget::default(), proofs, None, false)
@@ -431,7 +437,9 @@ async fn test_pay_invoice_twice() {
         Err(err) => match err {
             cdk::Error::RequestAlreadyPaid => (),
             err => {
-                panic!("Wrong invoice already paid: {}", err.to_string());
+                if !err.to_string().contains("Duplicate entry") {
+                    panic!("Wrong invoice already paid: {}", err.to_string());
+                }
             }
         },
         Ok(_) => {

+ 3 - 3
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -832,11 +832,11 @@ async fn test_concurrent_double_spend_melt() {
     let melt_request3 = melt_request.clone();
 
     // Spawn 3 concurrent tasks to process the melt requests
-    let task1 = tokio::spawn(async move { mint_clone1.melt_bolt11(&melt_request).await });
+    let task1 = tokio::spawn(async move { mint_clone1.melt(&melt_request).await });
 
-    let task2 = tokio::spawn(async move { mint_clone2.melt_bolt11(&melt_request2).await });
+    let task2 = tokio::spawn(async move { mint_clone2.melt(&melt_request2).await });
 
-    let task3 = tokio::spawn(async move { mint_clone3.melt_bolt11(&melt_request3).await });
+    let task3 = tokio::spawn(async move { mint_clone3.melt(&melt_request3).await });
 
     // Wait for all tasks to complete
     let results = tokio::try_join!(task1, task2, task3).expect("Tasks failed to complete");

+ 2 - 2
crates/cdk-integration-tests/tests/regtest.rs

@@ -89,7 +89,7 @@ async fn test_internal_payment() {
 
     let _melted = wallet.melt(&melt.id).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wait_for_mint_to_be_paid(&wallet_2, &mint_quote.id, 60)
         .await
         .unwrap();
 
@@ -348,7 +348,7 @@ async fn test_regtest_melt_amountless() {
 
     let mint_quote = wallet.mint_quote(mint_amount, None).await.unwrap();
 
-    assert_eq!(mint_quote.amount, mint_amount);
+    assert_eq!(mint_quote.amount, Some(mint_amount));
 
     lnd_client
         .pay_invoice(mint_quote.request)

+ 4 - 0
crates/cdk-integration-tests/tests/test_fees.rs

@@ -28,6 +28,10 @@ async fn test_swap() {
     let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
     pay_if_regtest(&invoice).await.unwrap();
 
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10)
+        .await
+        .unwrap();
+
     let _mint_amount = wallet
         .mint(&mint_quote.id, SplitTarget::default(), None)
         .await

+ 3 - 0
crates/cdk-lnbits/src/error.rs

@@ -14,6 +14,9 @@ pub enum Error {
     /// Amount overflow
     #[error("Amount overflow")]
     AmountOverflow,
+    /// Invalid payment hash
+    #[error("Invalid payment hash")]
+    InvalidPaymentHash,
     /// Anyhow error
     #[error(transparent)]
     Anyhow(#[from] anyhow::Error),

+ 197 - 144
crates/cdk-lnbits/src/lib.rs

@@ -6,7 +6,6 @@
 
 use std::cmp::max;
 use std::pin::Pin;
-use std::str::FromStr;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 
@@ -15,13 +14,14 @@ use async_trait::async_trait;
 use axum::Router;
 use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
 use cdk_common::common::FeeReserve;
-use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
+use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
-    PaymentQuoteResponse,
+    self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
+    MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
+    PaymentQuoteResponse, WaitPaymentResponse,
 };
-use cdk_common::util::unix_time;
-use cdk_common::{mint, Bolt11Invoice};
+use cdk_common::util::{hex, unix_time};
+use cdk_common::Bolt11Invoice;
 use error::Error;
 use futures::Stream;
 use lnbits_rs::api::invoice::CreateInvoiceRequest;
@@ -65,6 +65,7 @@ impl LNbits {
                 unit: CurrencyUnit::Sat,
                 invoice_description: true,
                 amountless: false,
+                bolt12: false,
             },
         })
     }
@@ -99,7 +100,7 @@ impl MintPayment for LNbits {
 
     async fn wait_any_incoming_payment(
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
+    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
         let api = self.lnbits_api.clone();
         let cancel_token = self.wait_invoice_cancel_token.clone();
         let is_active = Arc::clone(&self.wait_invoice_is_active);
@@ -122,23 +123,45 @@ impl MintPayment for LNbits {
                     msg_option = receiver.recv() => {
                         match msg_option {
                             Some(msg) => {
-                                let check = api.is_invoice_paid(&msg).await;
-
+                                let check = api.get_payment_info(&msg).await;
                                 match check {
-                                    Ok(state) => {
-                                        if state {
-                                            Some((msg, (api, cancel_token, is_active)))
+                                    Ok(payment) => {
+                                        if payment.paid {
+                                            match hex::decode(msg.clone()) {
+                                                Ok(decoded) => {
+                                                    match decoded.try_into() {
+                                                        Ok(hash) => {
+                                                            let response = WaitPaymentResponse {
+                                                                payment_identifier: PaymentIdentifier::PaymentHash(hash),
+                                                                payment_amount: Amount::from(payment.details.amount as u64),
+                                                                unit: CurrencyUnit::Sat,
+                                                                payment_id: msg.clone()
+                                                            };
+                                                            Some((response, (api, cancel_token, is_active)))
+                                                        },
+                                                        Err(e) => {
+                                                            tracing::error!("Failed to convert payment hash bytes to array: {:?}", e);
+                                                            None
+                                                        }
+                                                    }
+                                                },
+                                                Err(e) => {
+                                                    tracing::error!("Failed to decode payment hash hex string: {}", e);
+                                                    None
+                                                }
+                                            }
                                         } else {
-                                            Some(("".to_string(), (api, cancel_token, is_active)))
+                                            tracing::warn!("Received payment notification but could not check payment for {}", msg);
+                                            None
                                         }
-                                    }
-                                    _ => Some(("".to_string(), (api, cancel_token, is_active))),
+                                    },
+                                    Err(_) => None
                                 }
-                            }
+                            },
                             None => {
                                 is_active.store(false, Ordering::SeqCst);
                                 None
-                            },
+                            }
                         }
                     }
                 }
@@ -148,151 +171,181 @@ impl MintPayment for LNbits {
 
     async fn get_payment_quote(
         &self,
-        request: &str,
         unit: &CurrencyUnit,
-        options: Option<MeltOptions>,
+        options: OutgoingPaymentOptions,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
         if unit != &CurrencyUnit::Sat {
             return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
         }
 
-        let bolt11 = Bolt11Invoice::from_str(request)?;
-
-        let amount_msat = match options {
-            Some(amount) => {
-                if matches!(amount, MeltOptions::Mpp { mpp: _ }) {
-                    return Err(payment::Error::UnsupportedPaymentOption);
-                }
-                amount.amount_msat()
+        match options {
+            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
+                let amount_msat = match bolt11_options.melt_options {
+                    Some(amount) => {
+                        if matches!(amount, MeltOptions::Mpp { mpp: _ }) {
+                            return Err(payment::Error::UnsupportedPaymentOption);
+                        }
+                        amount.amount_msat()
+                    }
+                    None => bolt11_options
+                        .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;
+
+                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
+
+                let fee = max(relative_fee_reserve, absolute_fee_reserve);
+
+                Ok(PaymentQuoteResponse {
+                    request_lookup_id: PaymentIdentifier::PaymentHash(
+                        *bolt11_options.bolt11.payment_hash().as_ref(),
+                    ),
+                    amount,
+                    fee: fee.into(),
+                    state: MeltQuoteState::Unpaid,
+                    options: None,
+                    unit: unit.clone(),
+                })
             }
-            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;
-
-        let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
-
-        let fee = max(relative_fee_reserve, absolute_fee_reserve);
-
-        Ok(PaymentQuoteResponse {
-            request_lookup_id: bolt11.payment_hash().to_string(),
-            amount,
-            unit: unit.clone(),
-            fee: fee.into(),
-            state: MeltQuoteState::Unpaid,
-        })
+            OutgoingPaymentOptions::Bolt12(_bolt12_options) => {
+                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
+            }
+        }
     }
 
     async fn make_payment(
         &self,
-        melt_quote: mint::MeltQuote,
-        _partial_msats: Option<Amount>,
-        _max_fee_msats: Option<Amount>,
+        _unit: &CurrencyUnit,
+        options: OutgoingPaymentOptions,
     ) -> Result<MakePaymentResponse, Self::Err> {
-        let pay_response = self
-            .lnbits_api
-            .pay_invoice(&melt_quote.request, None)
-            .await
-            .map_err(|err| {
-                tracing::error!("Could not pay invoice");
-                tracing::error!("{}", err.to_string());
-                Self::Err::Anyhow(anyhow!("Could not pay invoice"))
-            })?;
-
-        let invoice_info = self
-            .lnbits_api
-            .get_payment_info(&pay_response.payment_hash)
-            .await
-            .map_err(|err| {
-                tracing::error!("Could not find invoice");
-                tracing::error!("{}", err.to_string());
-                Self::Err::Anyhow(anyhow!("Could not find invoice"))
-            })?;
-
-        let status = match invoice_info.paid {
-            true => MeltQuoteState::Paid,
-            false => MeltQuoteState::Unpaid,
-        };
-
-        let total_spent = Amount::from(
-            (invoice_info
-                .details
-                .amount
-                .checked_add(invoice_info.details.fee)
-                .ok_or(Error::AmountOverflow)?)
-            .unsigned_abs(),
-        );
-
-        Ok(MakePaymentResponse {
-            payment_lookup_id: pay_response.payment_hash,
-            payment_proof: invoice_info.details.preimage,
-            status,
-            total_spent,
-            unit: CurrencyUnit::Sat,
-        })
+        match options {
+            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
+                let pay_response = self
+                    .lnbits_api
+                    .pay_invoice(&bolt11_options.bolt11.to_string(), None)
+                    .await
+                    .map_err(|err| {
+                        tracing::error!("Could not pay invoice");
+                        tracing::error!("{}", err.to_string());
+                        Self::Err::Anyhow(anyhow!("Could not pay invoice"))
+                    })?;
+
+                let invoice_info = self
+                    .lnbits_api
+                    .get_payment_info(&pay_response.payment_hash)
+                    .await
+                    .map_err(|err| {
+                        tracing::error!("Could not find invoice");
+                        tracing::error!("{}", err.to_string());
+                        Self::Err::Anyhow(anyhow!("Could not find invoice"))
+                    })?;
+
+                let status = if invoice_info.paid {
+                    MeltQuoteState::Unpaid
+                } else {
+                    MeltQuoteState::Paid
+                };
+
+                let total_spent = Amount::from(
+                    (invoice_info
+                        .details
+                        .amount
+                        .checked_add(invoice_info.details.fee)
+                        .ok_or(Error::AmountOverflow)?)
+                    .unsigned_abs(),
+                );
+
+                Ok(MakePaymentResponse {
+                    payment_lookup_id: PaymentIdentifier::PaymentHash(
+                        hex::decode(pay_response.payment_hash)
+                            .map_err(|_| Error::InvalidPaymentHash)?
+                            .try_into()
+                            .map_err(|_| Error::InvalidPaymentHash)?,
+                    ),
+                    payment_proof: Some(invoice_info.details.payment_hash),
+                    status,
+                    total_spent,
+                    unit: CurrencyUnit::Sat,
+                })
+            }
+            OutgoingPaymentOptions::Bolt12(_) => {
+                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
+            }
+        }
     }
 
     async fn create_incoming_payment_request(
         &self,
-        amount: Amount,
         unit: &CurrencyUnit,
-        description: String,
-        unix_expiry: Option<u64>,
+        options: IncomingPaymentOptions,
     ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         if unit != &CurrencyUnit::Sat {
             return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
         }
 
-        let time_now = unix_time();
-
-        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,
-            webhook: self.webhook_url.clone(),
-            internal: None,
-            out: false,
-        };
-
-        let create_invoice_response = self
-            .lnbits_api
-            .create_invoice(&invoice_request)
-            .await
-            .map_err(|err| {
-                tracing::error!("Could not create invoice");
-                tracing::error!("{}", err.to_string());
-                Self::Err::Anyhow(anyhow!("Could not create invoice"))
-            })?;
-
-        let request: Bolt11Invoice = create_invoice_response
-            .bolt11()
-            .ok_or_else(|| Self::Err::Anyhow(anyhow!("Missing bolt11 invoice")))?
-            .parse()?;
-        let expiry = request.expires_at().map(|t| t.as_secs());
-
-        Ok(CreateIncomingPaymentResponse {
-            request_lookup_id: create_invoice_response.payment_hash().to_string(),
-            request: request.to_string(),
-            expiry,
-        })
+        match options {
+            IncomingPaymentOptions::Bolt11(bolt11_options) => {
+                let description = bolt11_options.description.unwrap_or_default();
+                let amount = bolt11_options.amount;
+                let unix_expiry = bolt11_options.unix_expiry;
+
+                let time_now = unix_time();
+                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,
+                    webhook: self.webhook_url.clone(),
+                    internal: None,
+                    out: false,
+                };
+
+                let create_invoice_response = self
+                    .lnbits_api
+                    .create_invoice(&invoice_request)
+                    .await
+                    .map_err(|err| {
+                        tracing::error!("Could not create invoice");
+                        tracing::error!("{}", err.to_string());
+                        Self::Err::Anyhow(anyhow!("Could not create invoice"))
+                    })?;
+
+                let request: Bolt11Invoice = create_invoice_response
+                    .bolt11()
+                    .ok_or_else(|| Self::Err::Anyhow(anyhow!("Missing bolt11 invoice")))?
+                    .parse()?;
+                let expiry = request.expires_at().map(|t| t.as_secs());
+
+                Ok(CreateIncomingPaymentResponse {
+                    request_lookup_id: PaymentIdentifier::PaymentHash(
+                        *request.payment_hash().as_ref(),
+                    ),
+                    request: request.to_string(),
+                    expiry,
+                })
+            }
+            IncomingPaymentOptions::Bolt12(_) => {
+                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
+            }
+        }
     }
 
     async fn check_incoming_payment_status(
         &self,
-        payment_hash: &str,
-    ) -> Result<MintQuoteState, Self::Err> {
-        let paid = self
+        payment_identifier: &PaymentIdentifier,
+    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
+        let payment = self
             .lnbits_api
-            .is_invoice_paid(payment_hash)
+            .get_payment_info(&payment_identifier.to_string())
             .await
             .map_err(|err| {
                 tracing::error!("Could not check invoice status");
@@ -300,21 +353,21 @@ impl MintPayment for LNbits {
                 Self::Err::Anyhow(anyhow!("Could not check invoice status"))
             })?;
 
-        let state = match paid {
-            true => MintQuoteState::Paid,
-            false => MintQuoteState::Unpaid,
-        };
-
-        Ok(state)
+        Ok(vec![WaitPaymentResponse {
+            payment_identifier: payment_identifier.clone(),
+            payment_amount: Amount::from(payment.details.amount as u64),
+            unit: CurrencyUnit::Sat,
+            payment_id: payment.details.payment_hash,
+        }])
     }
 
     async fn check_outgoing_payment(
         &self,
-        payment_hash: &str,
+        payment_identifier: &PaymentIdentifier,
     ) -> Result<MakePaymentResponse, Self::Err> {
         let payment = self
             .lnbits_api
-            .get_payment_info(payment_hash)
+            .get_payment_info(&payment_identifier.to_string())
             .await
             .map_err(|err| {
                 tracing::error!("Could not check invoice status");
@@ -323,7 +376,7 @@ impl MintPayment for LNbits {
             })?;
 
         let pay_response = MakePaymentResponse {
-            payment_lookup_id: payment.details.payment_hash,
+            payment_lookup_id: payment_identifier.clone(),
             payment_proof: payment.preimage,
             status: lnbits_to_melt_status(&payment.details.status, payment.details.pending),
             total_spent: Amount::from(

+ 293 - 235
crates/cdk-lnd/src/lib.rs

@@ -18,13 +18,14 @@ use async_trait::async_trait;
 use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
 use cdk_common::bitcoin::hashes::Hash;
 use cdk_common::common::FeeReserve;
-use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
+use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
-    PaymentQuoteResponse,
+    self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
+    MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
+    PaymentQuoteResponse, WaitPaymentResponse,
 };
 use cdk_common::util::hex;
-use cdk_common::{mint, Bolt11Invoice};
+use cdk_common::Bolt11Invoice;
 use error::Error;
 use futures::{Stream, StreamExt};
 use lnrpc::fee_limit::Limit;
@@ -39,6 +40,8 @@ pub mod error;
 mod proto;
 pub(crate) use proto::{lnrpc, routerrpc};
 
+use crate::lnrpc::invoice::InvoiceState;
+
 /// Lnd mint backend
 #[derive(Clone)]
 pub struct Lnd {
@@ -108,6 +111,7 @@ impl Lnd {
                 unit: CurrencyUnit::Msat,
                 invoice_description: true,
                 amountless: true,
+                bolt12: false,
             },
         })
     }
@@ -135,7 +139,7 @@ impl MintPayment for Lnd {
     #[instrument(skip_all)]
     async fn wait_any_incoming_payment(
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
+    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
         let mut lnd_client = self.lnd_client.clone();
 
         let stream_req = lnrpc::InvoiceSubscription {
@@ -176,8 +180,23 @@ impl MintPayment for Lnd {
 
                 match msg {
                     Ok(Some(msg)) => {
-                        if msg.state == 1 {
-                            Some((hex::encode(msg.r_hash), (stream, cancel_token, is_active)))
+                        if msg.state() == InvoiceState::Settled {
+
+                            let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
+
+                            if let Ok(hash_slice) = hash_slice {
+                            let hash = hex::encode(hash_slice);
+
+                              tracing::info!("LND: Processing payment with hash: {}", hash);
+                                            let wait_response = WaitPaymentResponse {
+                                                payment_identifier: PaymentIdentifier::PaymentHash(hash_slice), payment_amount: Amount::from(msg.amt_paid_msat as u64),
+                                                unit: CurrencyUnit::Msat,
+                                                payment_id: hash,
+                                            };
+                                            tracing::info!("LND: Created WaitPaymentResponse with amount {} msat", 
+                                                         msg.amt_paid_msat);
+                                            Some((wait_response, (stream, cancel_token, is_active)))
+                            }  else { None }
                         } else {
                             None
                         }
@@ -205,261 +224,299 @@ impl MintPayment for Lnd {
     #[instrument(skip_all)]
     async fn get_payment_quote(
         &self,
-        request: &str,
         unit: &CurrencyUnit,
-        options: Option<MeltOptions>,
+        options: OutgoingPaymentOptions,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        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(),
-        };
+        match options {
+            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
+                let amount_msat = match bolt11_options.melt_options {
+                    Some(amount) => amount.amount_msat(),
+                    None => bolt11_options
+                        .bolt11
+                        .amount_milli_satoshis()
+                        .ok_or(Error::UnknownInvoiceAmount)?
+                        .into(),
+                };
 
-        let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+                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;
+                let relative_fee_reserve =
+                    (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
 
-        let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
+                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
-        let fee = max(relative_fee_reserve, absolute_fee_reserve);
+                let fee = max(relative_fee_reserve, absolute_fee_reserve);
 
-        Ok(PaymentQuoteResponse {
-            request_lookup_id: bolt11.payment_hash().to_string(),
-            amount,
-            unit: unit.clone(),
-            fee: fee.into(),
-            state: MeltQuoteState::Unpaid,
-        })
+                Ok(PaymentQuoteResponse {
+                    request_lookup_id: PaymentIdentifier::PaymentHash(
+                        *bolt11_options.bolt11.payment_hash().as_ref(),
+                    ),
+                    amount,
+                    fee: fee.into(),
+                    state: MeltQuoteState::Unpaid,
+                    options: None,
+                    unit: unit.clone(),
+                })
+            }
+            OutgoingPaymentOptions::Bolt12(_) => {
+                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
+            }
+        }
     }
 
     #[instrument(skip_all)]
     async fn make_payment(
         &self,
-        melt_quote: mint::MeltQuote,
-        partial_amount: Option<Amount>,
-        max_fee: Option<Amount>,
+        _unit: &CurrencyUnit,
+        options: OutgoingPaymentOptions,
     ) -> Result<MakePaymentResponse, Self::Err> {
-        let payment_request = melt_quote.request;
-        let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
-
-        let pay_state = self
-            .check_outgoing_payment(&bolt11.payment_hash().to_string())
-            .await?;
-
-        match pay_state.status {
-            MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
-            MeltQuoteState::Paid => {
-                tracing::debug!("Melt attempted on invoice already paid");
-                return Err(Self::Err::InvoiceAlreadyPaid);
-            }
-            MeltQuoteState::Pending => {
-                tracing::debug!("Melt attempted on invoice already pending");
-                return Err(Self::Err::InvoicePaymentPending);
-            }
-        }
-
-        let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
-        let amount_msat: u64 = match bolt11.amount_milli_satoshis() {
-            Some(amount_msat) => amount_msat,
-            None => melt_quote
-                .msat_to_pay
-                .ok_or(Error::UnknownInvoiceAmount)?
-                .into(),
-        };
-
-        // Detect partial payments
-        match partial_amount {
-            Some(part_amt) => {
-                let partial_amount_msat = to_unit(part_amt, &melt_quote.unit, &CurrencyUnit::Msat)?;
-                let invoice = Bolt11Invoice::from_str(&payment_request)?;
-
-                // Extract information from invoice
-                let pub_key = invoice.get_payee_pub_key();
-                let payer_addr = invoice.payment_secret().0.to_vec();
-                let payment_hash = invoice.payment_hash();
-
-                let mut lnd_client = self.lnd_client.clone();
-
-                for attempt in 0..Self::MAX_ROUTE_RETRIES {
-                    // Create a request for the routes
-                    let route_req = lnrpc::QueryRoutesRequest {
-                        pub_key: hex::encode(pub_key.serialize()),
-                        amt_msat: u64::from(partial_amount_msat) as i64,
-                        fee_limit: max_fee.map(|f| {
-                            let limit = Limit::Fixed(u64::from(f) as i64);
-                            FeeLimit { limit: Some(limit) }
-                        }),
-                        use_mission_control: true,
-                        ..Default::default()
-                    };
-
-                    // Query the routes
-                    let mut routes_response = lnd_client
-                        .lightning()
-                        .query_routes(route_req)
-                        .await
-                        .map_err(Error::LndError)?
-                        .into_inner();
-
-                    // update its MPP record,
-                    // attempt it and check the result
-                    let last_hop: &mut Hop = routes_response.routes[0]
-                        .hops
-                        .last_mut()
-                        .ok_or(Error::MissingLastHop)?;
-                    let mpp_record = MppRecord {
-                        payment_addr: payer_addr.clone(),
-                        total_amt_msat: amount_msat as i64,
-                    };
-                    last_hop.mpp_record = Some(mpp_record);
+        match options {
+            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
+                let bolt11 = bolt11_options.bolt11;
+
+                let pay_state = self
+                    .check_outgoing_payment(&PaymentIdentifier::PaymentHash(
+                        *bolt11.payment_hash().as_ref(),
+                    ))
+                    .await?;
+
+                match pay_state.status {
+                    MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
+                    MeltQuoteState::Paid => {
+                        tracing::debug!("Melt attempted on invoice already paid");
+                        return Err(Self::Err::InvoiceAlreadyPaid);
+                    }
+                    MeltQuoteState::Pending => {
+                        tracing::debug!("Melt attempted on invoice already pending");
+                        return Err(Self::Err::InvoicePaymentPending);
+                    }
+                }
 
-                    let payment_response = lnd_client
-                        .router()
-                        .send_to_route_v2(routerrpc::SendToRouteRequest {
-                            payment_hash: payment_hash.to_byte_array().to_vec(),
-                            route: Some(routes_response.routes[0].clone()),
-                            ..Default::default()
-                        })
-                        .await
-                        .map_err(Error::LndError)?
-                        .into_inner();
-
-                    if let Some(failure) = payment_response.failure {
-                        if failure.code == 15 {
-                            tracing::debug!(
-                                "Attempt number {}: route has failed. Re-querying...",
-                                attempt + 1
-                            );
-                            continue;
+                // Detect partial payments
+                match bolt11_options.melt_options {
+                    Some(MeltOptions::Mpp { mpp }) => {
+                        let amount_msat: u64 = bolt11
+                            .amount_milli_satoshis()
+                            .ok_or(Error::UnknownInvoiceAmount)?;
+                        {
+                            let partial_amount_msat = mpp.amount;
+                            let invoice = bolt11;
+                            let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
+
+                            // Extract information from invoice
+                            let pub_key = invoice.get_payee_pub_key();
+                            let payer_addr = invoice.payment_secret().0.to_vec();
+                            let payment_hash = invoice.payment_hash();
+
+                            let mut lnd_client = self.lnd_client.clone();
+
+                            for attempt in 0..Self::MAX_ROUTE_RETRIES {
+                                // Create a request for the routes
+                                let route_req = lnrpc::QueryRoutesRequest {
+                                    pub_key: hex::encode(pub_key.serialize()),
+                                    amt_msat: u64::from(partial_amount_msat) as i64,
+                                    fee_limit: max_fee.map(|f| {
+                                        let limit = Limit::Fixed(u64::from(f) as i64);
+                                        FeeLimit { limit: Some(limit) }
+                                    }),
+                                    use_mission_control: true,
+                                    ..Default::default()
+                                };
+
+                                // Query the routes
+                                let mut routes_response = lnd_client
+                                    .lightning()
+                                    .query_routes(route_req)
+                                    .await
+                                    .map_err(Error::LndError)?
+                                    .into_inner();
+
+                                // update its MPP record,
+                                // attempt it and check the result
+                                let last_hop: &mut Hop = routes_response.routes[0]
+                                    .hops
+                                    .last_mut()
+                                    .ok_or(Error::MissingLastHop)?;
+                                let mpp_record = MppRecord {
+                                    payment_addr: payer_addr.clone(),
+                                    total_amt_msat: amount_msat as i64,
+                                };
+                                last_hop.mpp_record = Some(mpp_record);
+
+                                let payment_response = lnd_client
+                                    .router()
+                                    .send_to_route_v2(routerrpc::SendToRouteRequest {
+                                        payment_hash: payment_hash.to_byte_array().to_vec(),
+                                        route: Some(routes_response.routes[0].clone()),
+                                        ..Default::default()
+                                    })
+                                    .await
+                                    .map_err(Error::LndError)?
+                                    .into_inner();
+
+                                if let Some(failure) = payment_response.failure {
+                                    if failure.code == 15 {
+                                        tracing::debug!(
+                                            "Attempt number {}: route has failed. Re-querying...",
+                                            attempt + 1
+                                        );
+                                        continue;
+                                    }
+                                }
+
+                                // Get status and maybe the preimage
+                                let (status, payment_preimage) = match payment_response.status {
+                                    0 => (MeltQuoteState::Pending, None),
+                                    1 => (
+                                        MeltQuoteState::Paid,
+                                        Some(hex::encode(payment_response.preimage)),
+                                    ),
+                                    2 => (MeltQuoteState::Unpaid, None),
+                                    _ => (MeltQuoteState::Unknown, None),
+                                };
+
+                                // Get the actual amount paid in sats
+                                let mut total_amt: u64 = 0;
+                                if let Some(route) = payment_response.route {
+                                    total_amt = (route.total_amt_msat / 1000) as u64;
+                                }
+
+                                return Ok(MakePaymentResponse {
+                                    payment_lookup_id: PaymentIdentifier::PaymentHash(
+                                        payment_hash.to_byte_array(),
+                                    ),
+                                    payment_proof: payment_preimage,
+                                    status,
+                                    total_spent: total_amt.into(),
+                                    unit: CurrencyUnit::Sat,
+                                });
+                            }
+
+                            // "We have exhausted all tactical options" -- STEM, Upgrade (2018)
+                            // The payment was not possible within 50 retries.
+                            tracing::error!("Limit of retries reached, payment couldn't succeed.");
+                            Err(Error::PaymentFailed.into())
                         }
                     }
+                    _ => {
+                        let mut lnd_client = self.lnd_client.clone();
+
+                        let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
+
+                        let amount_msat = u64::from(
+                            bolt11_options
+                                .melt_options
+                                .map(|a| a.amount_msat())
+                                .unwrap_or_default(),
+                        );
+
+                        let pay_req = lnrpc::SendRequest {
+                            payment_request: bolt11.to_string(),
+                            fee_limit: max_fee.map(|f| {
+                                let limit = Limit::Fixed(u64::from(f) as i64);
+                                FeeLimit { limit: Some(limit) }
+                            }),
+                            amt_msat: amount_msat as i64,
+                            ..Default::default()
+                        };
+
+                        let payment_response = lnd_client
+                            .lightning()
+                            .send_payment_sync(tonic::Request::new(pay_req))
+                            .await
+                            .map_err(|err| {
+                                tracing::warn!("Lightning payment failed: {}", err);
+                                Error::PaymentFailed
+                            })?
+                            .into_inner();
+
+                        let total_amount = payment_response
+                            .payment_route
+                            .map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64)
+                            as u64;
+
+                        let (status, payment_preimage) = match total_amount == 0 {
+                            true => (MeltQuoteState::Unpaid, None),
+                            false => (
+                                MeltQuoteState::Paid,
+                                Some(hex::encode(payment_response.payment_preimage)),
+                            ),
+                        };
 
-                    // Get status and maybe the preimage
-                    let (status, payment_preimage) = match payment_response.status {
-                        0 => (MeltQuoteState::Pending, None),
-                        1 => (
-                            MeltQuoteState::Paid,
-                            Some(hex::encode(payment_response.preimage)),
-                        ),
-                        2 => (MeltQuoteState::Unpaid, None),
-                        _ => (MeltQuoteState::Unknown, None),
-                    };
+                        let payment_identifier =
+                            PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
 
-                    // Get the actual amount paid in sats
-                    let mut total_amt: u64 = 0;
-                    if let Some(route) = payment_response.route {
-                        total_amt = (route.total_amt_msat / 1000) as u64;
+                        Ok(MakePaymentResponse {
+                            payment_lookup_id: payment_identifier,
+                            payment_proof: payment_preimage,
+                            status,
+                            total_spent: total_amount.into(),
+                            unit: CurrencyUnit::Sat,
+                        })
                     }
-
-                    return Ok(MakePaymentResponse {
-                        payment_lookup_id: hex::encode(payment_hash),
-                        payment_proof: payment_preimage,
-                        status,
-                        total_spent: total_amt.into(),
-                        unit: CurrencyUnit::Sat,
-                    });
                 }
-
-                // "We have exhausted all tactical options" -- STEM, Upgrade (2018)
-                // The payment was not possible within 50 retries.
-                tracing::error!("Limit of retries reached, payment couldn't succeed.");
-                Err(Error::PaymentFailed.into())
             }
-            None => {
-                let mut lnd_client = self.lnd_client.clone();
-
-                let pay_req = lnrpc::SendRequest {
-                    payment_request,
-                    fee_limit: max_fee.map(|f| {
-                        let limit = Limit::Fixed(u64::from(f) as i64);
-                        FeeLimit { limit: Some(limit) }
-                    }),
-                    amt_msat: amount_msat as i64,
-                    ..Default::default()
-                };
-
-                let payment_response = lnd_client
-                    .lightning()
-                    .send_payment_sync(tonic::Request::new(pay_req))
-                    .await
-                    .map_err(|err| {
-                        tracing::warn!("Lightning payment failed: {}", err);
-                        Error::PaymentFailed
-                    })?
-                    .into_inner();
-
-                let total_amount = payment_response
-                    .payment_route
-                    .map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64)
-                    as u64;
-
-                let (status, payment_preimage) = match total_amount == 0 {
-                    true => (MeltQuoteState::Unpaid, None),
-                    false => (
-                        MeltQuoteState::Paid,
-                        Some(hex::encode(payment_response.payment_preimage)),
-                    ),
-                };
-
-                Ok(MakePaymentResponse {
-                    payment_lookup_id: hex::encode(payment_response.payment_hash),
-                    payment_proof: payment_preimage,
-                    status,
-                    total_spent: total_amount.into(),
-                    unit: CurrencyUnit::Sat,
-                })
+            OutgoingPaymentOptions::Bolt12(_) => {
+                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
             }
         }
     }
 
-    #[instrument(skip(self, description))]
+    #[instrument(skip(self, options))]
     async fn create_incoming_payment_request(
         &self,
-        amount: Amount,
         unit: &CurrencyUnit,
-        description: String,
-        unix_expiry: Option<u64>,
+        options: IncomingPaymentOptions,
     ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
-        let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
+        match options {
+            IncomingPaymentOptions::Bolt11(bolt11_options) => {
+                let description = bolt11_options.description.unwrap_or_default();
+                let amount = bolt11_options.amount;
+                let unix_expiry = bolt11_options.unix_expiry;
 
-        let invoice_request = lnrpc::Invoice {
-            value_msat: u64::from(amount) as i64,
-            memo: description,
-            ..Default::default()
-        };
+                let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
 
-        let mut lnd_client = self.lnd_client.clone();
+                let invoice_request = lnrpc::Invoice {
+                    value_msat: u64::from(amount_msat) as i64,
+                    memo: description,
+                    ..Default::default()
+                };
 
-        let invoice = lnd_client
-            .lightning()
-            .add_invoice(tonic::Request::new(invoice_request))
-            .await
-            .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
-            .into_inner();
+                let mut lnd_client = self.lnd_client.clone();
+
+                let invoice = lnd_client
+                    .lightning()
+                    .add_invoice(tonic::Request::new(invoice_request))
+                    .await
+                    .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
+                    .into_inner();
 
-        let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
+                let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
 
-        Ok(CreateIncomingPaymentResponse {
-            request_lookup_id: bolt11.payment_hash().to_string(),
-            request: bolt11.to_string(),
-            expiry: unix_expiry,
-        })
+                let payment_identifier =
+                    PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
+
+                Ok(CreateIncomingPaymentResponse {
+                    request_lookup_id: payment_identifier,
+                    request: bolt11.to_string(),
+                    expiry: unix_expiry,
+                })
+            }
+            IncomingPaymentOptions::Bolt12(_) => {
+                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
+            }
+        }
     }
 
     #[instrument(skip(self))]
     async fn check_incoming_payment_status(
         &self,
-        request_lookup_id: &str,
-    ) -> Result<MintQuoteState, Self::Err> {
+        payment_identifier: &PaymentIdentifier,
+    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
         let mut lnd_client = self.lnd_client.clone();
 
         let invoice_request = lnrpc::PaymentHash {
-            r_hash: hex::decode(request_lookup_id).unwrap(),
+            r_hash: hex::decode(payment_identifier.to_string()).unwrap(),
             ..Default::default()
         };
 
@@ -470,26 +527,27 @@ impl MintPayment for Lnd {
             .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
             .into_inner();
 
-        match invoice.state {
-            // Open
-            0 => Ok(MintQuoteState::Unpaid),
-            // Settled
-            1 => Ok(MintQuoteState::Paid),
-            // Canceled
-            2 => Ok(MintQuoteState::Unpaid),
-            // Accepted
-            3 => Ok(MintQuoteState::Unpaid),
-            _ => Err(Self::Err::Anyhow(anyhow!("Invalid status"))),
+        if invoice.state() == InvoiceState::Settled {
+            Ok(vec![WaitPaymentResponse {
+                payment_identifier: payment_identifier.clone(),
+                payment_amount: Amount::from(invoice.amt_paid_msat as u64),
+                unit: CurrencyUnit::Msat,
+                payment_id: hex::encode(invoice.r_hash),
+            }])
+        } else {
+            Ok(vec![])
         }
     }
 
     #[instrument(skip(self))]
     async fn check_outgoing_payment(
         &self,
-        payment_hash: &str,
+        payment_identifier: &PaymentIdentifier,
     ) -> Result<MakePaymentResponse, Self::Err> {
         let mut lnd_client = self.lnd_client.clone();
 
+        let payment_hash = &payment_identifier.to_string();
+
         let track_request = routerrpc::TrackPaymentRequest {
             payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
             no_inflight_updates: true,
@@ -503,7 +561,7 @@ impl MintPayment for Lnd {
                 let err_code = err.code();
                 if err_code == tonic::Code::NotFound {
                     return Ok(MakePaymentResponse {
-                        payment_lookup_id: payment_hash.to_string(),
+                        payment_lookup_id: payment_identifier.clone(),
                         payment_proof: None,
                         status: MeltQuoteState::Unknown,
                         total_spent: Amount::ZERO,
@@ -522,7 +580,7 @@ impl MintPayment for Lnd {
 
                     let response = match status {
                         PaymentStatus::Unknown => MakePaymentResponse {
-                            payment_lookup_id: payment_hash.to_string(),
+                            payment_lookup_id: payment_identifier.clone(),
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Unknown,
                             total_spent: Amount::ZERO,
@@ -533,7 +591,7 @@ impl MintPayment for Lnd {
                             continue;
                         }
                         PaymentStatus::Succeeded => MakePaymentResponse {
-                            payment_lookup_id: payment_hash.to_string(),
+                            payment_lookup_id: payment_identifier.clone(),
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Paid,
                             total_spent: Amount::from(
@@ -546,7 +604,7 @@ impl MintPayment for Lnd {
                             unit: CurrencyUnit::Sat,
                         },
                         PaymentStatus::Failed => MakePaymentResponse {
-                            payment_lookup_id: payment_hash.to_string(),
+                            payment_lookup_id: payment_identifier.clone(),
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Failed,
                             total_spent: Amount::ZERO,

+ 1 - 0
crates/cdk-mint-rpc/Cargo.toml

@@ -23,6 +23,7 @@ anyhow.workspace = true
 cdk = { workspace = true, features = [
     "mint",
 ] }
+cdk-common = { workspace = true }
 clap.workspace = true
 tonic = { workspace = true, features = ["transport"] }
 tracing.workspace = true

+ 41 - 8
crates/cdk-mint-rpc/src/proto/server.rs

@@ -3,12 +3,13 @@ use std::path::PathBuf;
 use std::str::FromStr;
 use std::sync::Arc;
 
-use cdk::mint::Mint;
+use cdk::mint::{Mint, MintQuote};
 use cdk::nuts::nut04::MintMethodSettings;
 use cdk::nuts::nut05::MeltMethodSettings;
 use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
 use cdk::types::QuoteTTL;
 use cdk::Amount;
+use cdk_common::payment::WaitPaymentResponse;
 use thiserror::Error;
 use tokio::sync::Notify;
 use tokio::task::JoinHandle;
@@ -650,15 +651,47 @@ impl CdkMint for MintRPCServer {
 
         match state {
             MintQuoteState::Paid => {
+                // Create a dummy payment response
+                let response = WaitPaymentResponse {
+                    payment_id: String::new(),
+                    payment_amount: mint_quote.amount_paid(),
+                    unit: mint_quote.unit.clone(),
+                    payment_identifier: mint_quote.request_lookup_id.clone(),
+                };
+
+                let mut tx = self
+                    .mint
+                    .localstore
+                    .begin_transaction()
+                    .await
+                    .map_err(|_| Status::internal("Could not start db transaction".to_string()))?;
+
                 self.mint
-                    .pay_mint_quote(&mint_quote)
+                    .pay_mint_quote(&mut tx, &mint_quote, response)
                     .await
-                    .map_err(|_| Status::internal("Could not find quote".to_string()))?;
+                    .map_err(|_| Status::internal("Could not process payment".to_string()))?;
+
+                tx.commit()
+                    .await
+                    .map_err(|_| Status::internal("Could not commit db transaction".to_string()))?;
             }
             _ => {
-                let mut mint_quote = mint_quote;
-
-                mint_quote.state = state;
+                // Create a new quote with the same values
+                let quote = MintQuote::new(
+                    Some(mint_quote.id),                  // id
+                    mint_quote.request.clone(),           // request
+                    mint_quote.unit.clone(),              // unit
+                    mint_quote.amount,                    // amount
+                    mint_quote.expiry,                    // expiry
+                    mint_quote.request_lookup_id.clone(), // request_lookup_id
+                    mint_quote.pubkey,                    // pubkey
+                    mint_quote.amount_issued(),           // amount_issued
+                    mint_quote.amount_paid(),             // amount_paid
+                    mint_quote.payment_method.clone(),    // method
+                    0,                                    // created_at
+                    vec![],                               // blinded_messages
+                    vec![],                               // payment_ids
+                );
 
                 let mut tx = self
                     .mint
@@ -666,7 +699,7 @@ impl CdkMint for MintRPCServer {
                     .begin_transaction()
                     .await
                     .map_err(|_| Status::internal("Could not update quote".to_string()))?;
-                tx.add_or_replace_mint_quote(mint_quote)
+                tx.add_mint_quote(quote.clone())
                     .await
                     .map_err(|_| Status::internal("Could not update quote".to_string()))?;
                 tx.commit()
@@ -684,7 +717,7 @@ impl CdkMint for MintRPCServer {
             .ok_or(Status::invalid_argument("Could not find quote".to_string()))?;
 
         Ok(Response::new(UpdateNut04QuoteRequest {
-            state: mint_quote.state.to_string(),
+            state: mint_quote.state().to_string(),
             quote_id: mint_quote.id.to_string(),
         }))
     }

+ 56 - 33
crates/cdk-mintd/src/main.rs

@@ -431,6 +431,21 @@ async fn configure_backend_for_unit(
     mint_melt_limits: MintMeltLimits,
     backend: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
 ) -> Result<MintBuilder> {
+    let payment_settings = backend.get_settings().await?;
+
+    if let Some(bolt12) = payment_settings.get("bolt12") {
+        if bolt12.as_bool().unwrap_or_default() {
+            mint_builder = mint_builder
+                .add_ln_backend(
+                    unit.clone(),
+                    PaymentMethod::Bolt12,
+                    mint_melt_limits,
+                    Arc::clone(&backend),
+                )
+                .await?;
+        }
+    }
+
     mint_builder = mint_builder
         .add_ln_backend(
             unit.clone(),
@@ -651,39 +666,6 @@ async fn start_services(
     let listen_port = settings.info.listen_port;
     let cache: HttpCache = settings.info.http_cache.clone().into();
 
-    let v1_service =
-        cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache).await?;
-
-    let mut mint_service = Router::new()
-        .merge(v1_service)
-        .layer(
-            ServiceBuilder::new()
-                .layer(RequestDecompressionLayer::new())
-                .layer(CompressionLayer::new()),
-        )
-        .layer(TraceLayer::new_for_http());
-
-    #[cfg(feature = "swagger")]
-    {
-        if settings.info.enable_swagger_ui.unwrap_or(false) {
-            mint_service = mint_service.merge(
-                utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
-                    .url("/api-docs/openapi.json", cdk_axum::ApiDocV1::openapi()),
-            );
-        }
-    }
-
-    for router in ln_routers {
-        mint_service = mint_service.merge(router);
-    }
-
-    let shutdown = Arc::new(Notify::new());
-    let mint_clone = Arc::clone(&mint);
-    tokio::spawn({
-        let shutdown = Arc::clone(&shutdown);
-        async move { mint_clone.wait_for_paid_invoices(shutdown).await }
-    });
-
     #[cfg(feature = "management-rpc")]
     let mut rpc_enabled = false;
     #[cfg(not(feature = "management-rpc"))]
@@ -742,6 +724,47 @@ async fn start_services(
         mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?;
     }
 
+    let mint_info = mint.mint_info().await?;
+    let nut04_methods = mint_info.nuts.nut04.supported_methods();
+    let nut05_methods = mint_info.nuts.nut05.supported_methods();
+
+    let bolt12_supported = nut04_methods.contains(&&PaymentMethod::Bolt12)
+        || nut05_methods.contains(&&PaymentMethod::Bolt12);
+
+    let v1_service =
+        cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, bolt12_supported)
+            .await?;
+
+    let mut mint_service = Router::new()
+        .merge(v1_service)
+        .layer(
+            ServiceBuilder::new()
+                .layer(RequestDecompressionLayer::new())
+                .layer(CompressionLayer::new()),
+        )
+        .layer(TraceLayer::new_for_http());
+
+    #[cfg(feature = "swagger")]
+    {
+        if settings.info.enable_swagger_ui.unwrap_or(false) {
+            mint_service = mint_service.merge(
+                utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
+                    .url("/api-docs/openapi.json", cdk_axum::ApiDocV1::openapi()),
+            );
+        }
+    }
+
+    for router in ln_routers {
+        mint_service = mint_service.merge(router);
+    }
+
+    let shutdown = Arc::new(Notify::new());
+    let mint_clone = Arc::clone(&mint);
+    tokio::spawn({
+        let shutdown = Arc::clone(&shutdown);
+        async move { mint_clone.wait_for_paid_invoices(shutdown).await }
+    });
+
     let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
 
     let listener = tokio::net::TcpListener::bind(socket_addr).await?;

+ 4 - 1
crates/cdk-payment-processor/Cargo.toml

@@ -25,6 +25,7 @@ lnd = ["dep:cdk-lnd"]
 anyhow.workspace = true
 async-trait.workspace = true
 bitcoin.workspace = true
+cashu.workspace = true
 cdk-common = { workspace = true, features = ["mint"] }
 cdk-cln = { workspace = true, optional = true }
 cdk-lnd = { workspace = true, optional = true }
@@ -34,7 +35,7 @@ thiserror.workspace = true
 tracing.workspace = true
 tracing-subscriber.workspace = true
 lightning-invoice.workspace = true
-uuid = { workspace = true, optional = true }
+uuid = { workspace = true }
 utoipa = { workspace = true, optional = true }
 futures.workspace = true
 serde_json.workspace = true
@@ -43,6 +44,8 @@ tonic = { workspace = true, features = ["router"] }
 prost.workspace = true
 tokio-stream.workspace = true
 tokio-util = { workspace = true, default-features = false }
+hex = "0.4"
+lightning = { workspace = true }
 
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]

+ 64 - 3
crates/cdk-payment-processor/src/error.rs

@@ -1,6 +1,7 @@
-//! Errors
+//! Error for payment processor
 
 use thiserror::Error;
+use tonic::Status;
 
 /// CDK Payment processor error
 #[derive(Debug, Error)]
@@ -8,13 +9,73 @@ pub enum Error {
     /// Invalid ID
     #[error("Invalid id")]
     InvalidId,
+    /// Invalid payment identifier
+    #[error("Invalid payment identifier")]
+    InvalidPaymentIdentifier,
+    /// Invalid hash
+    #[error("Invalid hash")]
+    InvalidHash,
+    /// Invalid currency unit
+    #[error("Invalid currency unit: {0}")]
+    InvalidCurrencyUnit(String),
+    /// Parse invoice error
+    #[error(transparent)]
+    Invoice(#[from] lightning_invoice::ParseOrSemanticError),
+    /// Hex decode error
+    #[error(transparent)]
+    Hex(#[from] hex::FromHexError),
+    /// BOLT12 parse error
+    #[error("BOLT12 parse error")]
+    Bolt12Parse,
     /// 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
+    /// Payment error
     #[error(transparent)]
-    Invoice(#[from] lightning_invoice::ParseOrSemanticError),
+    Payment(#[from] cdk_common::payment::Error),
+}
+
+impl From<Error> for Status {
+    fn from(error: Error) -> Self {
+        match error {
+            Error::InvalidId => Status::invalid_argument("Invalid ID"),
+            Error::InvalidPaymentIdentifier => {
+                Status::invalid_argument("Invalid payment identifier")
+            }
+            Error::InvalidHash => Status::invalid_argument("Invalid hash"),
+            Error::InvalidCurrencyUnit(unit) => {
+                Status::invalid_argument(format!("Invalid currency unit: {unit}"))
+            }
+            Error::Invoice(err) => Status::invalid_argument(format!("Invoice error: {err}")),
+            Error::Hex(err) => Status::invalid_argument(format!("Hex decode error: {err}")),
+            Error::Bolt12Parse => Status::invalid_argument("BOLT12 parse error"),
+            Error::NUT00(err) => Status::internal(format!("NUT00 error: {err}")),
+            Error::NUT05(err) => Status::internal(format!("NUT05 error: {err}")),
+            Error::Payment(err) => Status::internal(format!("Payment error: {err}")),
+        }
+    }
+}
+
+impl From<Error> for cdk_common::payment::Error {
+    fn from(error: Error) -> Self {
+        match error {
+            Error::InvalidId => Self::Custom("Invalid ID".to_string()),
+            Error::InvalidPaymentIdentifier => {
+                Self::Custom("Invalid payment identifier".to_string())
+            }
+            Error::InvalidHash => Self::Custom("Invalid hash".to_string()),
+            Error::InvalidCurrencyUnit(unit) => {
+                Self::Custom(format!("Invalid currency unit: {unit}"))
+            }
+            Error::Invoice(err) => Self::Custom(format!("Invoice error: {err}")),
+            Error::Hex(err) => Self::Custom(format!("Hex decode error: {err}")),
+            Error::Bolt12Parse => Self::Custom("BOLT12 parse error".to_string()),
+            Error::NUT00(err) => Self::Custom(format!("NUT00 error: {err}")),
+            Error::NUT05(err) => err.into(),
+            Error::Payment(err) => err,
+        }
+    }
 }

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

@@ -3,6 +3,7 @@
 #![warn(rustdoc::bare_urls)]
 
 pub mod error;
+/// Protocol types and functionality for the CDK payment processor
 pub mod proto;
 
 pub use proto::cdk_payment_processor_client::CdkPaymentProcessorClient;

+ 115 - 47
crates/cdk-payment-processor/src/proto/client.rs

@@ -1,15 +1,14 @@
 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,
+    CreateIncomingPaymentResponse, IncomingPaymentOptions as CdkIncomingPaymentOptions,
+    MakePaymentResponse as CdkMakePaymentResponse, MintPayment,
+    PaymentQuoteResponse as CdkPaymentQuoteResponse, WaitPaymentResponse,
 };
-use cdk_common::{mint, Amount, CurrencyUnit, MeltOptions, MintQuoteState};
 use futures::{Stream, StreamExt};
 use serde_json::Value;
 use tokio_util::sync::CancellationToken;
@@ -17,10 +16,10 @@ 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,
+use crate::proto::cdk_payment_processor_client::CdkPaymentProcessorClient;
+use crate::proto::{
+    CheckIncomingPaymentRequest, CheckOutgoingPaymentRequest, CreatePaymentRequest, EmptyRequest,
+    IncomingPaymentOptions, MakePaymentRequest, OutgoingPaymentRequestType, PaymentQuoteRequest,
 };
 
 /// Payment Processor
@@ -100,7 +99,7 @@ impl MintPayment for PaymentProcessorClient {
     async fn get_settings(&self) -> Result<Value, Self::Err> {
         let mut inner = self.inner.clone();
         let response = inner
-            .get_settings(Request::new(SettingsRequest {}))
+            .get_settings(Request::new(EmptyRequest {}))
             .await
             .map_err(|err| {
                 tracing::error!("Could not get settings: {}", err);
@@ -115,18 +114,36 @@ impl MintPayment for PaymentProcessorClient {
     /// Create a new invoice
     async fn create_incoming_payment_request(
         &self,
-        amount: Amount,
-        unit: &CurrencyUnit,
-        description: String,
-        unix_expiry: Option<u64>,
+        unit: &cdk_common::CurrencyUnit,
+        options: CdkIncomingPaymentOptions,
     ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         let mut inner = self.inner.clone();
+
+        let proto_options = match options {
+            CdkIncomingPaymentOptions::Bolt11(opts) => IncomingPaymentOptions {
+                options: Some(super::incoming_payment_options::Options::Bolt11(
+                    super::Bolt11IncomingPaymentOptions {
+                        description: opts.description,
+                        amount: opts.amount.into(),
+                        unix_expiry: opts.unix_expiry,
+                    },
+                )),
+            },
+            CdkIncomingPaymentOptions::Bolt12(opts) => IncomingPaymentOptions {
+                options: Some(super::incoming_payment_options::Options::Bolt12(
+                    super::Bolt12IncomingPaymentOptions {
+                        description: opts.description,
+                        amount: opts.amount.map(Into::into),
+                        unix_expiry: opts.unix_expiry,
+                    },
+                )),
+            },
+        };
+
         let response = inner
             .create_payment(Request::new(CreatePaymentRequest {
-                amount: amount.into(),
                 unit: unit.to_string(),
-                description,
-                unix_expiry,
+                options: Some(proto_options),
             }))
             .await
             .map_err(|err| {
@@ -143,16 +160,36 @@ impl MintPayment for PaymentProcessorClient {
 
     async fn get_payment_quote(
         &self,
-        request: &str,
-        unit: &CurrencyUnit,
-        options: Option<MeltOptions>,
-    ) -> Result<PaymentQuoteResponse, Self::Err> {
+        unit: &cdk_common::CurrencyUnit,
+        options: cdk_common::payment::OutgoingPaymentOptions,
+    ) -> Result<CdkPaymentQuoteResponse, Self::Err> {
         let mut inner = self.inner.clone();
+
+        let request_type = match &options {
+            cdk_common::payment::OutgoingPaymentOptions::Bolt11(_) => {
+                OutgoingPaymentRequestType::Bolt11Invoice
+            }
+            cdk_common::payment::OutgoingPaymentOptions::Bolt12(_) => {
+                OutgoingPaymentRequestType::Bolt12Offer
+            }
+        };
+
+        let proto_request = match &options {
+            cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.bolt11.to_string(),
+            cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => opts.offer.to_string(),
+        };
+
+        let proto_options = match &options {
+            cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.melt_options,
+            cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => opts.melt_options,
+        };
+
         let response = inner
-            .get_payment_quote(Request::new(super::PaymentQuoteRequest {
-                request: request.to_string(),
+            .get_payment_quote(Request::new(PaymentQuoteRequest {
+                request: proto_request,
                 unit: unit.to_string(),
-                options: options.map(|o| o.into()),
+                options: proto_options.map(Into::into),
+                request_type: request_type.into(),
             }))
             .await
             .map_err(|err| {
@@ -167,16 +204,44 @@ impl MintPayment for PaymentProcessorClient {
 
     async fn make_payment(
         &self,
-        melt_quote: mint::MeltQuote,
-        partial_amount: Option<Amount>,
-        max_fee_amount: Option<Amount>,
+        _unit: &cdk_common::CurrencyUnit,
+        options: cdk_common::payment::OutgoingPaymentOptions,
     ) -> Result<CdkMakePaymentResponse, Self::Err> {
         let mut inner = self.inner.clone();
+
+        let payment_options = match options {
+            cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => {
+                super::OutgoingPaymentVariant {
+                    options: Some(super::outgoing_payment_variant::Options::Bolt11(
+                        super::Bolt11OutgoingPaymentOptions {
+                            bolt11: opts.bolt11.to_string(),
+                            max_fee_amount: opts.max_fee_amount.map(Into::into),
+                            timeout_secs: opts.timeout_secs,
+                            melt_options: opts.melt_options.map(Into::into),
+                        },
+                    )),
+                }
+            }
+            cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => {
+                super::OutgoingPaymentVariant {
+                    options: Some(super::outgoing_payment_variant::Options::Bolt12(
+                        super::Bolt12OutgoingPaymentOptions {
+                            offer: opts.offer.to_string(),
+                            max_fee_amount: opts.max_fee_amount.map(Into::into),
+                            timeout_secs: opts.timeout_secs,
+                            invoice: opts.invoice,
+                            melt_options: opts.melt_options.map(Into::into),
+                        },
+                    )),
+                }
+            }
+        };
+
         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()),
+                payment_options: Some(payment_options),
+                partial_amount: None,
+                max_fee_amount: None,
             }))
             .await
             .map_err(|err| {
@@ -198,17 +263,16 @@ impl MintPayment for PaymentProcessorClient {
         })?)
     }
 
-    /// 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> {
+    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + 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 {})
+            .wait_incoming_payment(EmptyRequest {})
             .await
             .map_err(|err| {
                 tracing::error!("Could not check incoming payment stream: {}", err);
@@ -222,15 +286,18 @@ impl MintPayment for PaymentProcessorClient {
 
         let transformed_stream = stream
             .take_until(cancel_fut)
-            .filter_map(|item| async move {
+            .filter_map(|item| async {
                 match item {
-                    Ok(value) => {
-                        tracing::warn!("{}", value.lookup_id);
-                        Some(value.lookup_id)
-                    }
+                    Ok(value) => match value.try_into() {
+                        Ok(payment_response) => Some(payment_response),
+                        Err(e) => {
+                            tracing::error!("Error converting payment response: {}", e);
+                            None
+                        }
+                    },
                     Err(e) => {
                         tracing::error!("Error in payment stream: {}", e);
-                        None // Skip this item and continue with the stream
+                        None
                     }
                 }
             })
@@ -255,12 +322,12 @@ impl MintPayment for PaymentProcessorClient {
 
     async fn check_incoming_payment_status(
         &self,
-        request_lookup_id: &str,
-    ) -> Result<MintQuoteState, Self::Err> {
+        payment_identifier: &cdk_common::payment::PaymentIdentifier,
+    ) -> Result<Vec<WaitPaymentResponse>, 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(),
+                request_identifier: Some(payment_identifier.clone().into()),
             }))
             .await
             .map_err(|err| {
@@ -269,20 +336,21 @@ impl MintPayment for PaymentProcessorClient {
             })?;
 
         let check_incoming = response.into_inner();
-
-        let status = check_incoming.status().as_str_name();
-
-        Ok(MintQuoteState::from_str(status)?)
+        check_incoming
+            .payments
+            .into_iter()
+            .map(|resp| resp.try_into().map_err(Self::Err::from))
+            .collect()
     }
 
     async fn check_outgoing_payment(
         &self,
-        request_lookup_id: &str,
+        payment_identifier: &cdk_common::payment::PaymentIdentifier,
     ) -> 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(),
+                request_identifier: Some(payment_identifier.clone().into()),
             }))
             .await
             .map_err(|err| {

+ 172 - 110
crates/cdk-payment-processor/src/proto/mod.rs

@@ -1,12 +1,11 @@
-//! Proto types for payment processor
-
 use std::str::FromStr;
 
 use cdk_common::payment::{
     CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse,
+    PaymentIdentifier as CdkPaymentIdentifier, WaitPaymentResponse,
 };
-use cdk_common::{Bolt11Invoice, CurrencyUnit, MeltQuoteBolt11Request};
-use melt_options::Options;
+use cdk_common::{CurrencyUnit, MeltOptions as CdkMeltOptions};
+
 mod client;
 mod server;
 
@@ -15,15 +14,85 @@ pub use server::PaymentProcessorServer;
 
 tonic::include_proto!("cdk_payment_processor");
 
+impl From<CdkPaymentIdentifier> for PaymentIdentifier {
+    fn from(value: CdkPaymentIdentifier) -> Self {
+        match value {
+            CdkPaymentIdentifier::Label(id) => Self {
+                r#type: PaymentIdentifierType::Label.into(),
+                value: Some(payment_identifier::Value::Id(id)),
+            },
+            CdkPaymentIdentifier::OfferId(id) => Self {
+                r#type: PaymentIdentifierType::OfferId.into(),
+                value: Some(payment_identifier::Value::Id(id)),
+            },
+            CdkPaymentIdentifier::PaymentHash(hash) => Self {
+                r#type: PaymentIdentifierType::PaymentHash.into(),
+                value: Some(payment_identifier::Value::Hash(hex::encode(hash))),
+            },
+            CdkPaymentIdentifier::Bolt12PaymentHash(hash) => Self {
+                r#type: PaymentIdentifierType::Bolt12PaymentHash.into(),
+                value: Some(payment_identifier::Value::Hash(hex::encode(hash))),
+            },
+            CdkPaymentIdentifier::CustomId(id) => Self {
+                r#type: PaymentIdentifierType::CustomId.into(),
+                value: Some(payment_identifier::Value::Id(id)),
+            },
+        }
+    }
+}
+
+impl TryFrom<PaymentIdentifier> for CdkPaymentIdentifier {
+    type Error = crate::error::Error;
+
+    fn try_from(value: PaymentIdentifier) -> Result<Self, Self::Error> {
+        match (value.r#type(), value.value) {
+            (PaymentIdentifierType::Label, Some(payment_identifier::Value::Id(id))) => {
+                Ok(CdkPaymentIdentifier::Label(id))
+            }
+            (PaymentIdentifierType::OfferId, Some(payment_identifier::Value::Id(id))) => {
+                Ok(CdkPaymentIdentifier::OfferId(id))
+            }
+            (PaymentIdentifierType::PaymentHash, Some(payment_identifier::Value::Hash(hash))) => {
+                let decoded = hex::decode(hash)?;
+                let hash_array: [u8; 32] = decoded
+                    .try_into()
+                    .map_err(|_| crate::error::Error::InvalidHash)?;
+                Ok(CdkPaymentIdentifier::PaymentHash(hash_array))
+            }
+            (
+                PaymentIdentifierType::Bolt12PaymentHash,
+                Some(payment_identifier::Value::Hash(hash)),
+            ) => {
+                let decoded = hex::decode(hash)?;
+                let hash_array: [u8; 32] = decoded
+                    .try_into()
+                    .map_err(|_| crate::error::Error::InvalidHash)?;
+                Ok(CdkPaymentIdentifier::Bolt12PaymentHash(hash_array))
+            }
+            (PaymentIdentifierType::CustomId, Some(payment_identifier::Value::Id(id))) => {
+                Ok(CdkPaymentIdentifier::CustomId(id))
+            }
+            _ => Err(crate::error::Error::InvalidPaymentIdentifier),
+        }
+    }
+}
+
 impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
     type Error = crate::error::Error;
     fn try_from(value: MakePaymentResponse) -> Result<Self, Self::Error> {
+        let status = value.status().as_str_name().parse()?;
+        let payment_proof = value.payment_proof;
+        let total_spent = value.total_spent.into();
+        let unit = CurrencyUnit::from_str(&value.unit)?;
+        let payment_identifier = value
+            .payment_identifier
+            .ok_or(crate::error::Error::InvalidPaymentIdentifier)?;
         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()?,
+            payment_lookup_id: payment_identifier.try_into()?,
+            payment_proof,
+            status,
+            total_spent,
+            unit,
         })
     }
 }
@@ -31,8 +100,8 @@ impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
 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(),
+            payment_identifier: Some(value.payment_lookup_id.into()),
+            payment_proof: value.payment_proof,
             status: QuoteState::from(value.status).into(),
             total_spent: value.total_spent.into(),
             unit: value.unit.to_string(),
@@ -43,8 +112,8 @@ impl From<CdkMakePaymentResponse> for MakePaymentResponse {
 impl From<CreateIncomingPaymentResponse> for CreatePaymentResponse {
     fn from(value: CreateIncomingPaymentResponse) -> Self {
         Self {
-            request_lookup_id: value.request_lookup_id,
-            request: value.request.to_string(),
+            request_identifier: Some(value.request_lookup_id.into()),
+            request: value.request,
             expiry: value.expiry,
         }
     }
@@ -54,82 +123,102 @@ impl TryFrom<CreatePaymentResponse> for CreateIncomingPaymentResponse {
     type Error = crate::error::Error;
 
     fn try_from(value: CreatePaymentResponse) -> Result<Self, Self::Error> {
+        let request_identifier = value
+            .request_identifier
+            .ok_or(crate::error::Error::InvalidPaymentIdentifier)?;
         Ok(Self {
-            request_lookup_id: value.request_lookup_id,
+            request_lookup_id: request_identifier.try_into()?,
             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,
+            request_identifier: Some(value.request_lookup_id.into()),
             amount: value.amount.into(),
             fee: value.fee.into(),
-            state: QuoteState::from(value.state).into(),
             unit: value.unit.to_string(),
+            state: QuoteState::from(value.state).into(),
+            melt_options: value.options.map(|opt| match opt {
+                cdk_common::payment::PaymentQuoteOptions::Bolt12 { invoice } => {
+                    PaymentQuoteOptions {
+                        melt_options: Some(payment_quote_options::MeltOptions::Bolt12(
+                            Bolt12Options {
+                                invoice: invoice.map(String::from_utf8).and_then(|r| r.ok()),
+                            },
+                        )),
+                    }
+                }
+            }),
         }
     }
 }
 
-impl From<cdk_common::nut23::MeltOptions> for MeltOptions {
-    fn from(value: cdk_common::nut23::MeltOptions) -> Self {
-        Self {
-            options: Some(value.into()),
-        }
-    }
-}
+impl From<PaymentQuoteResponse> for cdk_common::payment::PaymentQuoteResponse {
+    fn from(value: PaymentQuoteResponse) -> Self {
+        let state_val = value.state();
+        let request_identifier = value
+            .request_identifier
+            .expect("request identifier required");
 
-impl From<cdk_common::nut23::MeltOptions> for Options {
-    fn from(value: cdk_common::nut23::MeltOptions) -> Self {
-        match value {
-            cdk_common::MeltOptions::Mpp { mpp } => Self::Mpp(Mpp {
-                amount: mpp.amount.into(),
-            }),
-            cdk_common::MeltOptions::Amountless { amountless } => Self::Amountless(Amountless {
-                amount_msat: amountless.amount_msat.into(),
+        Self {
+            request_lookup_id: request_identifier
+                .try_into()
+                .expect("valid request identifier"),
+            amount: value.amount.into(),
+            fee: value.fee.into(),
+            unit: CurrencyUnit::from_str(&value.unit).unwrap_or_default(),
+            state: state_val.into(),
+            options: value.melt_options.map(|opt| match opt.melt_options {
+                Some(payment_quote_options::MeltOptions::Bolt12(bolt12)) => {
+                    cdk_common::payment::PaymentQuoteOptions::Bolt12 {
+                        invoice: bolt12.invoice.as_deref().map(str::as_bytes).map(Vec::from),
+                    }
+                }
+                None => unreachable!(),
             }),
         }
     }
 }
 
-impl From<MeltOptions> for cdk_common::nut23::MeltOptions {
+impl From<MeltOptions> for CdkMeltOptions {
     fn from(value: MeltOptions) -> Self {
-        let options = value.options.expect("option defined");
-        match options {
-            Options::Mpp(mpp) => cdk_common::MeltOptions::new_mpp(mpp.amount),
-            Options::Amountless(amountless) => {
-                cdk_common::MeltOptions::new_amountless(amountless.amount_msat)
-            }
+        match value.options.expect("option defined") {
+            melt_options::Options::Mpp(mpp) => Self::Mpp {
+                mpp: cashu::nuts::nut15::Mpp {
+                    amount: mpp.amount.into(),
+                },
+            },
+            melt_options::Options::Amountless(amountless) => Self::Amountless {
+                amountless: cashu::nuts::nut23::Amountless {
+                    amount_msat: amountless.amount_msat.into(),
+                },
+            },
         }
     }
 }
 
-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(),
-            unit: CurrencyUnit::from_str(&value.unit).unwrap_or_default(),
-            fee: value.fee.into(),
-            state: value.state().into(),
+impl From<CdkMeltOptions> for MeltOptions {
+    fn from(value: CdkMeltOptions) -> Self {
+        match value {
+            CdkMeltOptions::Mpp { mpp } => Self {
+                options: Some(melt_options::Options::Mpp(Mpp {
+                    amount: mpp.amount.into(),
+                })),
+            },
+            CdkMeltOptions::Amountless { amountless } => Self {
+                options: Some(melt_options::Options::Amountless(Amountless {
+                    amount_msat: amountless.amount_msat.into(),
+                })),
+            },
         }
     }
 }
 
-impl From<QuoteState> for cdk_common::nut05::QuoteState {
+impl From<QuoteState> for cdk_common::nuts::MeltQuoteState {
     fn from(value: QuoteState) -> Self {
         match value {
             QuoteState::Unpaid => Self::Unpaid,
@@ -142,80 +231,53 @@ impl From<QuoteState> for cdk_common::nut05::QuoteState {
     }
 }
 
-impl From<cdk_common::nut05::QuoteState> for QuoteState {
-    fn from(value: cdk_common::nut05::QuoteState) -> Self {
+impl From<cdk_common::nuts::MeltQuoteState> for QuoteState {
+    fn from(value: cdk_common::nuts::MeltQuoteState) -> 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,
+            cdk_common::nuts::MeltQuoteState::Unpaid => Self::Unpaid,
+            cdk_common::nuts::MeltQuoteState::Paid => Self::Paid,
+            cdk_common::nuts::MeltQuoteState::Pending => Self::Pending,
+            cdk_common::nuts::MeltQuoteState::Unknown => Self::Unknown,
+            cdk_common::nuts::MeltQuoteState::Failed => Self::Failed,
         }
     }
 }
 
-impl From<cdk_common::nut23::QuoteState> for QuoteState {
-    fn from(value: cdk_common::nut23::QuoteState) -> Self {
+impl From<cdk_common::nuts::MintQuoteState> for QuoteState {
+    fn from(value: cdk_common::nuts::MintQuoteState) -> 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,
+            cdk_common::nuts::MintQuoteState::Unpaid => Self::Unpaid,
+            cdk_common::nuts::MintQuoteState::Paid => Self::Paid,
+            cdk_common::nuts::MintQuoteState::Issued => Self::Issued,
         }
     }
 }
 
-impl From<cdk_common::mint::MeltQuote> for MeltQuote {
-    fn from(value: cdk_common::mint::MeltQuote) -> Self {
+impl From<WaitPaymentResponse> for WaitIncomingPaymentResponse {
+    fn from(value: WaitPaymentResponse) -> Self {
         Self {
-            id: value.id.to_string(),
+            payment_identifier: Some(value.payment_identifier.into()),
+            payment_amount: value.payment_amount.into(),
             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()),
-            created_time: value.created_time,
-            paid_time: value.paid_time,
+            payment_id: value.payment_id,
         }
     }
 }
 
-impl TryFrom<MeltQuote> for cdk_common::mint::MeltQuote {
+impl TryFrom<WaitIncomingPaymentResponse> for WaitPaymentResponse {
     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()),
-            created_time: value.created_time,
-            paid_time: value.paid_time,
-        })
-    }
-}
-
-impl TryFrom<PaymentQuoteRequest> for MeltQuoteBolt11Request {
-    type Error = crate::error::Error;
+    fn try_from(value: WaitIncomingPaymentResponse) -> Result<Self, Self::Error> {
+        let payment_identifier = value
+            .payment_identifier
+            .ok_or(crate::error::Error::InvalidPaymentIdentifier)?
+            .try_into()?;
 
-    fn try_from(value: PaymentQuoteRequest) -> Result<Self, Self::Error> {
         Ok(Self {
-            request: Bolt11Invoice::from_str(&value.request)?,
+            payment_identifier,
+            payment_amount: value.payment_amount.into(),
             unit: CurrencyUnit::from_str(&value.unit)?,
-            options: value.options.map(|o| o.into()),
+            payment_id: value.payment_id,
         })
     }
 }

+ 98 - 35
crates/cdk-payment-processor/src/proto/payment_processor.proto

@@ -3,30 +3,73 @@ syntax = "proto3";
 package cdk_payment_processor;
 
 service CdkPaymentProcessor {  
-    rpc GetSettings(SettingsRequest) returns (SettingsResponse) {}
+    rpc GetSettings(EmptyRequest) 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) {}
+    rpc WaitIncomingPayment(EmptyRequest) returns (stream WaitIncomingPaymentResponse) {}
 }
 
-message SettingsRequest {}
+message EmptyRequest {}
 
 message SettingsResponse {
   string inner = 1;
 }
 
+message Bolt11IncomingPaymentOptions {
+  optional string description = 1;
+  uint64 amount = 2;
+  optional uint64 unix_expiry = 3;
+}
+
+message Bolt12IncomingPaymentOptions {
+  optional string description = 1;
+  optional uint64 amount = 2;
+  optional uint64 unix_expiry = 3;
+}
+
+enum PaymentMethodType {
+  BOLT11 = 0;
+  BOLT12 = 1;
+}
+
+enum OutgoingPaymentRequestType {
+  BOLT11_INVOICE = 0;
+  BOLT12_OFFER = 1;
+}
+
+enum PaymentIdentifierType {
+  PAYMENT_HASH = 0;
+  OFFER_ID = 1;
+  LABEL = 2;
+  BOLT12_PAYMENT_HASH = 3;
+  CUSTOM_ID = 4;
+}
+
+message PaymentIdentifier {
+  PaymentIdentifierType type = 1;
+  oneof value {
+    string hash = 2; // Used for PAYMENT_HASH and BOLT12_PAYMENT_HASH
+    string id = 3;   // Used for OFFER_ID, LABEL, and CUSTOM_ID
+  }
+}
+
+message IncomingPaymentOptions {
+  oneof options {
+    Bolt11IncomingPaymentOptions bolt11 = 1;
+    Bolt12IncomingPaymentOptions bolt12 = 2;
+  }
+}
+
 message CreatePaymentRequest {
-  uint64 amount = 1;
-  string unit = 2;
-  string description = 3;
-  optional uint64 unix_expiry = 4;
+  string unit = 1;
+  IncomingPaymentOptions options = 2;
 }
 
 message CreatePaymentResponse {
-  string request_lookup_id = 1;
+  PaymentIdentifier request_identifier = 1;
   string request = 2;
   optional uint64 expiry = 3;
 }
@@ -35,7 +78,6 @@ message Mpp {
     uint64 amount = 1;
 }
 
-
 message Amountless {
     uint64 amount_msat = 1;
 }
@@ -51,6 +93,7 @@ message PaymentQuoteRequest {
   string request = 1;
   string unit = 2;
   optional MeltOptions options = 3;
+  OutgoingPaymentRequestType request_type = 4;
 }
 
 enum QuoteState {
@@ -62,38 +105,60 @@ enum QuoteState {
     ISSUED = 5;
 }
 
+message Bolt12Options {
+  optional string invoice = 1;
+}
+
+message PaymentQuoteOptions {
+  oneof melt_options {
+    Bolt12Options bolt12 = 1;
+  }
+}
 
 message PaymentQuoteResponse {
-  string request_lookup_id = 1;
+  PaymentIdentifier request_identifier = 1;
   uint64 amount = 2;
   uint64 fee = 3;
   QuoteState state = 4;
-  string unit = 5;
+  optional PaymentQuoteOptions melt_options = 5;
+  string unit = 6;
+}
+
+message Bolt11OutgoingPaymentOptions {
+  string bolt11 = 1;
+  optional uint64 max_fee_amount = 2;
+  optional uint64 timeout_secs = 3;
+  optional MeltOptions melt_options = 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;
-    uint64 created_time = 11;
-    optional uint64 paid_time = 12;
+message Bolt12OutgoingPaymentOptions {
+  string offer = 1;
+  optional uint64 max_fee_amount = 2;
+  optional uint64 timeout_secs = 3;
+  optional bytes invoice = 4;
+  optional MeltOptions melt_options = 5;
+}
+
+enum OutgoingPaymentOptionsType {
+  OUTGOING_BOLT11 = 0;
+  OUTGOING_BOLT12 = 1;
+}
+
+message OutgoingPaymentVariant {
+  oneof options {
+    Bolt11OutgoingPaymentOptions bolt11 = 1;
+    Bolt12OutgoingPaymentOptions bolt12 = 2;
+  }
 }
 
 message MakePaymentRequest {
-  MeltQuote melt_quote = 1;
+  OutgoingPaymentVariant payment_options = 1;
   optional uint64 partial_amount = 2;
   optional uint64 max_fee_amount = 3;
 }
 
 message MakePaymentResponse {
-  string payment_lookup_id = 1;
+  PaymentIdentifier payment_identifier = 1;
   optional string payment_proof = 2;
   QuoteState status = 3;
   uint64 total_spent = 4;
@@ -101,22 +166,20 @@ message MakePaymentResponse {
 }
 
 message CheckIncomingPaymentRequest {
-  string request_lookup_id = 1;
+  PaymentIdentifier request_identifier = 1;
 }
 
 message CheckIncomingPaymentResponse {
-  QuoteState status = 1;
+  repeated WaitIncomingPaymentResponse payments = 1;
 }
 
 message CheckOutgoingPaymentRequest {
-  string request_lookup_id = 1;
+  PaymentIdentifier request_identifier = 1;
 }
 
-
-message WaitIncomingPaymentRequest {
-}
-
-
 message WaitIncomingPaymentResponse {
-  string lookup_id = 1;
+  PaymentIdentifier payment_identifier = 1;
+  uint64 payment_amount = 2;
+  string unit = 3;
+  string payment_id = 4;
 }

+ 154 - 62
crates/cdk-payment-processor/src/proto/server.rs

@@ -5,8 +5,10 @@ use std::str::FromStr;
 use std::sync::Arc;
 use std::time::Duration;
 
-use cdk_common::payment::MintPayment;
+use cdk_common::payment::{IncomingPaymentOptions, MintPayment};
+use cdk_common::CurrencyUnit;
 use futures::{Stream, StreamExt};
+use lightning::offers::offer::Offer;
 use serde_json::Value;
 use tokio::sync::{mpsc, Notify};
 use tokio::task::JoinHandle;
@@ -17,6 +19,7 @@ use tonic::{async_trait, Request, Response, Status};
 use tracing::instrument;
 
 use super::cdk_payment_processor_server::{CdkPaymentProcessor, CdkPaymentProcessorServer};
+use crate::error::Error;
 use crate::proto::*;
 
 type ResponseStream =
@@ -162,7 +165,7 @@ impl Drop for PaymentProcessorServer {
 impl CdkPaymentProcessor for PaymentProcessorServer {
     async fn get_settings(
         &self,
-        _request: Request<SettingsRequest>,
+        _request: Request<EmptyRequest>,
     ) -> Result<Response<SettingsResponse>, Status> {
         let settings: Value = self
             .inner
@@ -179,18 +182,36 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
         &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 CreatePaymentRequest { unit, options } = request.into_inner();
+
+        let unit = CurrencyUnit::from_str(&unit)
+            .map_err(|_| Status::invalid_argument("Invalid currency unit"))?;
+
+        let options = options.ok_or_else(|| Status::invalid_argument("Missing payment options"))?;
+
+        let proto_options = match options
+            .options
+            .ok_or_else(|| Status::invalid_argument("Missing options"))?
+        {
+            incoming_payment_options::Options::Bolt11(opts) => {
+                IncomingPaymentOptions::Bolt11(cdk_common::payment::Bolt11IncomingPaymentOptions {
+                    description: opts.description,
+                    amount: opts.amount.into(),
+                    unix_expiry: opts.unix_expiry,
+                })
+            }
+            incoming_payment_options::Options::Bolt12(opts) => IncomingPaymentOptions::Bolt12(
+                Box::new(cdk_common::payment::Bolt12IncomingPaymentOptions {
+                    description: opts.description,
+                    amount: opts.amount.map(Into::into),
+                    unix_expiry: opts.unix_expiry,
+                }),
+            ),
+        };
+
         let invoice_response = self
             .inner
-            .create_incoming_payment_request(amount.into(), &unit, description, unix_expiry)
+            .create_incoming_payment_request(&unit, proto_options)
             .await
             .map_err(|_| Status::internal("Could not create invoice"))?;
 
@@ -203,21 +224,46 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
     ) -> Result<Response<PaymentQuoteResponse>, Status> {
         let request = request.into_inner();
 
-        let options: Option<cdk_common::MeltOptions> =
-            request.options.as_ref().map(|options| (*options).into());
+        let unit = CurrencyUnit::from_str(&request.unit)
+            .map_err(|_| Status::invalid_argument("Invalid currency unit"))?;
+
+        let options = match request.request_type() {
+            OutgoingPaymentRequestType::Bolt11Invoice => {
+                let bolt11: cdk_common::Bolt11Invoice =
+                    request.request.parse().map_err(Error::Invoice)?;
+
+                cdk_common::payment::OutgoingPaymentOptions::Bolt11(Box::new(
+                    cdk_common::payment::Bolt11OutgoingPaymentOptions {
+                        bolt11,
+                        max_fee_amount: None,
+                        timeout_secs: None,
+                        melt_options: request.options.map(Into::into),
+                    },
+                ))
+            }
+            OutgoingPaymentRequestType::Bolt12Offer => {
+                // Parse offer to verify it's valid, but store as string
+                let _: Offer = request.request.parse().map_err(|_| Error::Bolt12Parse)?;
+
+                cdk_common::payment::OutgoingPaymentOptions::Bolt12(Box::new(
+                    cdk_common::payment::Bolt12OutgoingPaymentOptions {
+                        offer: Offer::from_str(&request.request).unwrap(),
+                        max_fee_amount: None,
+                        timeout_secs: None,
+                        invoice: None,
+                        melt_options: request.options.map(Into::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,
-            )
+            .get_payment_quote(&unit, options)
             .await
             .map_err(|err| {
-                tracing::error!("Could not get bolt11 melt quote: {}", err);
-                Status::internal("Could not get melt quote")
+                tracing::error!("Could not get payment quote: {}", err);
+                Status::internal("Could not get quote")
             })?;
 
         Ok(Response::new(payment_quote.into()))
@@ -229,17 +275,51 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
     ) -> Result<Response<MakePaymentResponse>, Status> {
         let request = request.into_inner();
 
-        let pay_invoice = self
+        let options = request
+            .payment_options
+            .ok_or_else(|| Status::invalid_argument("Missing payment options"))?;
+
+        let (unit, payment_options) = match options
+            .options
+            .ok_or_else(|| Status::invalid_argument("Missing options"))?
+        {
+            outgoing_payment_variant::Options::Bolt11(opts) => {
+                let bolt11: cdk_common::Bolt11Invoice =
+                    opts.bolt11.parse().map_err(Error::Invoice)?;
+
+                let payment_options = cdk_common::payment::OutgoingPaymentOptions::Bolt11(
+                    Box::new(cdk_common::payment::Bolt11OutgoingPaymentOptions {
+                        bolt11,
+                        max_fee_amount: opts.max_fee_amount.map(Into::into),
+                        timeout_secs: opts.timeout_secs,
+                        melt_options: opts.melt_options.map(Into::into),
+                    }),
+                );
+
+                (CurrencyUnit::Msat, payment_options)
+            }
+            outgoing_payment_variant::Options::Bolt12(opts) => {
+                let offer = Offer::from_str(&opts.offer)
+                    .map_err(|_| Error::Bolt12Parse)
+                    .unwrap();
+
+                let payment_options = cdk_common::payment::OutgoingPaymentOptions::Bolt12(
+                    Box::new(cdk_common::payment::Bolt12OutgoingPaymentOptions {
+                        offer,
+                        max_fee_amount: opts.max_fee_amount.map(Into::into),
+                        timeout_secs: opts.timeout_secs,
+                        invoice: opts.invoice,
+                        melt_options: opts.melt_options.map(Into::into),
+                    }),
+                );
+
+                (CurrencyUnit::Msat, payment_options)
+            }
+        };
+
+        let pay_response = 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()),
-            )
+            .make_payment(&unit, payment_options)
             .await
             .map_err(|err| {
                 tracing::error!("Could not make payment: {}", err);
@@ -255,7 +335,7 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
                 }
             })?;
 
-        Ok(Response::new(pay_invoice.into()))
+        Ok(Response::new(pay_response.into()))
     }
 
     async fn check_incoming_payment(
@@ -264,14 +344,20 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
     ) -> Result<Response<CheckIncomingPaymentResponse>, Status> {
         let request = request.into_inner();
 
-        let check_response = self
+        let payment_identifier = request
+            .request_identifier
+            .ok_or_else(|| Status::invalid_argument("Missing request identifier"))?
+            .try_into()
+            .map_err(|_| Status::invalid_argument("Invalid request identifier"))?;
+
+        let check_responses = self
             .inner
-            .check_incoming_payment_status(&request.request_lookup_id)
+            .check_incoming_payment_status(&payment_identifier)
             .await
             .map_err(|_| Status::internal("Could not check incoming payment status"))?;
 
         Ok(Response::new(CheckIncomingPaymentResponse {
-            status: QuoteState::from(check_response).into(),
+            payments: check_responses.into_iter().map(|r| r.into()).collect(),
         }))
     }
 
@@ -281,23 +367,28 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
     ) -> Result<Response<MakePaymentResponse>, Status> {
         let request = request.into_inner();
 
+        let payment_identifier = request
+            .request_identifier
+            .ok_or_else(|| Status::invalid_argument("Missing request identifier"))?
+            .try_into()
+            .map_err(|_| Status::invalid_argument("Invalid request identifier"))?;
+
         let check_response = self
             .inner
-            .check_outgoing_payment(&request.request_lookup_id)
+            .check_outgoing_payment(&payment_identifier)
             .await
-            .map_err(|_| Status::internal("Could not check incoming payment status"))?;
+            .map_err(|_| Status::internal("Could not check outgoing 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>,
+        _request: Request<EmptyRequest>,
     ) -> Result<Response<Self::WaitIncomingPaymentStream>, Status> {
         tracing::debug!("Server waiting for payment stream");
         let (tx, rx) = mpsc::channel(128);
@@ -307,34 +398,35 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
         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);
+                    _ = shutdown_clone.notified() => {
+                        tracing::info!("Shutdown signal received, stopping task");
+                        ln.cancel_wait_invoice();
                         break;
                     }
-                }
+                    result = ln.wait_any_incoming_payment() => {
+                        match result {
+                            Ok(mut stream) => {
+                                while let Some(payment_response) = stream.next().await {
+                                    match tx.send(Result::<_, Status>::Ok(payment_response.into()))
+                                    .await
+                                    {
+                                        Ok(_) => {
+                                            // Response was queued to be sent to client
+                                        }
+                                        Err(item) => {
+                                            tracing::error!("Error adding incoming payment to stream: {}", item);
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                            Err(err) => {
+                                tracing::warn!("Could not get invoice stream: {}", err);
+                                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
                             }
-                        }
-                        Err(err) => {
-                            tracing::warn!("Could not get invoice stream for {}", err);
-
-                            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
                         }
                     }
                 }
-                }
             }
         });
 

+ 3 - 0
crates/cdk-sqlite/src/mint/error.rs

@@ -101,6 +101,9 @@ pub enum Error {
     /// Invalid keyset ID
     #[error("Invalid keyset ID")]
     InvalidKeysetId,
+    /// Invalid melt payment request
+    #[error("Invalid melt payment request")]
+    InvalidMeltPaymentRequest,
 }
 
 impl From<Error> for cdk_common::database::Error {

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

@@ -44,7 +44,7 @@ pub async fn new_with_state(
     let mut tx = MintDatabase::begin_transaction(&db).await?;
 
     for quote in mint_quotes {
-        tx.add_or_replace_mint_quote(quote).await?;
+        tx.add_mint_quote(quote).await?;
     }
 
     for quote in melt_quotes {

+ 1 - 0
crates/cdk-sqlite/src/mint/migrations.rs

@@ -21,4 +21,5 @@ pub static MIGRATIONS: &[(&str, &str)] = &[
     ("20250406093755_mint_created_time_signature.sql", include_str!(r#"./migrations/20250406093755_mint_created_time_signature.sql"#)),
     ("20250415093121_drop_keystore_foreign.sql", include_str!(r#"./migrations/20250415093121_drop_keystore_foreign.sql"#)),
     ("20250626120251_rename_blind_message_y_to_b.sql", include_str!(r#"./migrations/20250626120251_rename_blind_message_y_to_b.sql"#)),
+    ("20250706101057_bolt12.sql", include_str!(r#"./migrations/20250706101057_bolt12.sql"#)),
 ];

+ 81 - 0
crates/cdk-sqlite/src/mint/migrations/20250706101057_bolt12.sql

@@ -0,0 +1,81 @@
+-- Add new columns to mint_quote table
+ALTER TABLE mint_quote ADD COLUMN amount_paid INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE mint_quote ADD COLUMN amount_issued INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE mint_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'BOLT11';
+ALTER TABLE mint_quote DROP COLUMN issued_time;
+ALTER TABLE mint_quote DROP COLUMN paid_time;
+
+-- Set amount_paid equal to amount for quotes with PAID or ISSUED state
+UPDATE mint_quote SET amount_paid = amount WHERE state = 'PAID' OR state = 'ISSUED';
+
+-- Set amount_issued equal to amount for quotes with ISSUED state
+UPDATE mint_quote SET amount_issued = amount WHERE state = 'ISSUED';
+
+DROP INDEX IF EXISTS mint_quote_state_index;
+
+-- Remove the state column from mint_quote table
+ALTER TABLE mint_quote DROP COLUMN state;
+
+-- Remove NOT NULL constraint from amount column
+CREATE TABLE mint_quote_temp (
+    id TEXT PRIMARY KEY,
+    amount INTEGER,
+    unit TEXT NOT NULL,
+    request TEXT NOT NULL,
+    expiry INTEGER NOT NULL,
+    request_lookup_id TEXT UNIQUE,
+    pubkey TEXT,
+    created_time INTEGER NOT NULL DEFAULT 0,
+    amount_paid INTEGER NOT NULL DEFAULT 0,
+    amount_issued INTEGER NOT NULL DEFAULT 0,
+    payment_method TEXT NOT NULL DEFAULT 'BOLT11'
+);
+
+INSERT INTO mint_quote_temp (id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, amount_paid, amount_issued, payment_method) 
+SELECT id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, amount_paid, amount_issued, payment_method 
+FROM mint_quote;
+
+DROP TABLE mint_quote;
+ALTER TABLE mint_quote_temp RENAME TO mint_quote;
+
+ALTER TABLE mint_quote ADD COLUMN request_lookup_id_kind TEXT NOT NULL DEFAULT 'payment_hash';
+
+CREATE INDEX IF NOT EXISTS idx_mint_quote_created_time ON mint_quote(created_time);
+CREATE INDEX IF NOT EXISTS idx_mint_quote_expiry ON mint_quote(expiry);
+CREATE INDEX IF NOT EXISTS idx_mint_quote_request_lookup_id ON mint_quote(request_lookup_id);
+CREATE INDEX IF NOT EXISTS idx_mint_quote_request_lookup_id_and_kind ON mint_quote(request_lookup_id, request_lookup_id_kind);
+
+-- Create mint_quote_payments table
+CREATE TABLE mint_quote_payments (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    quote_id TEXT NOT NULL,
+    payment_id TEXT NOT NULL UNIQUE,
+    timestamp INTEGER NOT NULL,
+    amount INTEGER NOT NULL,
+    FOREIGN KEY (quote_id) REFERENCES mint_quote(id)
+);
+
+-- Create index on payment_id for faster lookups
+CREATE INDEX idx_mint_quote_payments_payment_id ON mint_quote_payments(payment_id);
+CREATE INDEX idx_mint_quote_payments_quote_id ON mint_quote_payments(quote_id);
+
+-- Create mint_quote_issued table
+CREATE TABLE mint_quote_issued (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    quote_id TEXT NOT NULL,
+    amount INTEGER NOT NULL,
+    timestamp INTEGER NOT NULL,
+    FOREIGN KEY (quote_id) REFERENCES mint_quote(id)
+);
+
+-- Create index on quote_id for faster lookups
+CREATE INDEX idx_mint_quote_issued_quote_id ON mint_quote_issued(quote_id);
+
+-- Add new columns to melt_quote table
+ALTER TABLE melt_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'bolt11';
+ALTER TABLE melt_quote ADD COLUMN options TEXT;
+ALTER TABLE melt_quote ADD COLUMN request_lookup_id_kind TEXT NOT NULL DEFAULT 'payment_hash';
+
+CREATE INDEX IF NOT EXISTS idx_melt_quote_request_lookup_id_and_kind ON mint_quote(request_lookup_id, request_lookup_id_kind);
+
+ALTER TABLE melt_quote DROP COLUMN msat_to_pay;

ファイルの差分が大きいため隠しています
+ 480 - 224
crates/cdk-sqlite/src/mint/mod.rs


+ 1 - 0
crates/cdk-sqlite/src/wallet/migrations.rs

@@ -17,4 +17,5 @@ pub static MIGRATIONS: &[(&str, &str)] = &[
     ("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)),
     ("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)),
     ("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/20250616144830_add_keyset_expiry.sql"#)),
+    ("20250707093445_bolt12.sql", include_str!(r#"./migrations/20250707093445_bolt12.sql"#)),
 ];

+ 58 - 0
crates/cdk-sqlite/src/wallet/migrations/20250707093445_bolt12.sql

@@ -0,0 +1,58 @@
+ALTER TABLE mint_quote ADD COLUMN amount_paid INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE mint_quote ADD COLUMN amount_minted INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE mint_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'BOLT11';
+
+-- Remove NOT NULL constraint from amount column
+PRAGMA foreign_keys=off;
+CREATE TABLE mint_quote_new (
+    id TEXT PRIMARY KEY,
+    mint_url TEXT NOT NULL,
+    payment_method TEXT NOT NULL DEFAULT 'bolt11',
+    amount INTEGER,
+    unit TEXT NOT NULL,
+    request TEXT NOT NULL,
+    state TEXT NOT NULL,
+    expiry INTEGER NOT NULL,
+    amount_paid INTEGER NOT NULL DEFAULT 0,
+    amount_issued INTEGER NOT NULL DEFAULT 0,
+    secret_key TEXT
+);
+
+-- Explicitly specify columns for proper mapping
+INSERT INTO mint_quote_new (
+    id, 
+    mint_url, 
+    payment_method,
+    amount, 
+    unit, 
+    request, 
+    state, 
+    expiry, 
+    amount_paid,
+    amount_issued,
+    secret_key
+) 
+SELECT 
+    id, 
+    mint_url, 
+    'bolt11', -- Default value for the new payment_method column
+    amount, 
+    unit, 
+    request, 
+    state, 
+    expiry, 
+    0, -- Default value for amount_paid
+    0, -- Default value for amount_minted
+    secret_key
+FROM mint_quote;
+
+DROP TABLE mint_quote;
+ALTER TABLE mint_quote_new RENAME TO mint_quote;
+PRAGMA foreign_keys=on;
+
+
+-- Set amount_paid equal to amount for quotes with PAID or ISSUED state
+UPDATE mint_quote SET amount_paid = amount WHERE state = 'PAID' OR state = 'ISSUED';
+
+-- Set amount_issued equal to amount for quotes with ISSUED state
+UPDATE mint_quote SET amount_issued = amount WHERE state = 'ISSUED';

+ 30 - 10
crates/cdk-sqlite/src/wallet/mod.rs

@@ -14,8 +14,8 @@ use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
 use cdk_common::secret::Secret;
 use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
 use cdk_common::{
-    database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, Proof, ProofDleq,
-    PublicKey, SecretKey, SpendingConditions, State,
+    database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PaymentMethod, Proof,
+    ProofDleq, PublicKey, SecretKey, SpendingConditions, State,
 };
 use error::Error;
 use tracing::instrument;
@@ -376,9 +376,9 @@ ON CONFLICT(mint_url) DO UPDATE SET
         Statement::new(
             r#"
 INSERT INTO mint_quote
-(id, mint_url, amount, unit, request, state, expiry, secret_key)
+(id, mint_url, amount, unit, request, state, expiry, secret_key, payment_method, amount_issued, amount_paid)
 VALUES
-(:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key)
+(:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key, :payment_method, :amount_issued, :amount_paid)
 ON CONFLICT(id) DO UPDATE SET
     mint_url = excluded.mint_url,
     amount = excluded.amount,
@@ -386,18 +386,24 @@ ON CONFLICT(id) DO UPDATE SET
     request = excluded.request,
     state = excluded.state,
     expiry = excluded.expiry,
-    secret_key = excluded.secret_key
+    secret_key = excluded.secret_key,
+    payment_method = excluded.payment_method,
+    amount_issued = excluded.amount_issued,
+    amount_paid = excluded.amount_paid
 ;
         "#,
         )
         .bind(":id", quote.id.to_string())
         .bind(":mint_url", quote.mint_url.to_string())
-        .bind(":amount", u64::from(quote.amount) as i64)
+        .bind(":amount", quote.amount.map(|a| a.to_i64()))
         .bind(":unit", quote.unit.to_string())
         .bind(":request", quote.request)
         .bind(":state", quote.state.to_string())
         .bind(":expiry", quote.expiry as i64)
         .bind(":secret_key", quote.secret_key.map(|p| p.to_string()))
+        .bind(":payment_method", quote.payment_method.to_string())
+        .bind(":amount_issued", quote.amount_issued.to_i64())
+        .bind(":amount_paid", quote.amount_paid.to_i64())
         .execute(&self.pool.get().map_err(Error::Pool)?)
         .map_err(Error::Sqlite)?;
 
@@ -416,7 +422,10 @@ ON CONFLICT(id) DO UPDATE SET
                 request,
                 state,
                 expiry,
-                secret_key
+                secret_key,
+                payment_method,
+                amount_issued,
+                amount_paid
             FROM
                 mint_quote
             WHERE
@@ -950,16 +959,24 @@ fn sqlite_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
             request,
             state,
             expiry,
-            secret_key
+            secret_key,
+            row_method,
+            row_amount_minted,
+            row_amount_paid
         ) = row
     );
 
-    let amount: u64 = column_as_number!(amount);
+    let amount: Option<i64> = column_as_nullable_number!(amount);
+
+    let amount_paid: u64 = column_as_number!(row_amount_paid);
+    let amount_minted: u64 = column_as_number!(row_amount_minted);
+    let payment_method =
+        PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
 
     Ok(MintQuote {
         id: column_as_string!(id),
         mint_url: column_as_string!(mint_url, MintUrl::from_str),
-        amount: Amount::from(amount),
+        amount: amount.and_then(Amount::from_i64),
         unit: column_as_string!(unit, CurrencyUnit::from_str),
         request: column_as_string!(request),
         state: column_as_string!(state, MintQuoteState::from_str),
@@ -967,6 +984,9 @@ fn sqlite_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
         secret_key: column_as_nullable_string!(secret_key)
             .map(|v| SecretKey::from_str(&v))
             .transpose()?,
+        payment_method,
+        amount_issued: amount_minted.into(),
+        amount_paid: amount_paid.into(),
     })
 }
 

+ 1 - 0
crates/cdk/Cargo.toml

@@ -28,6 +28,7 @@ async-trait.workspace = true
 anyhow.workspace = true
 bitcoin.workspace = true
 ciborium.workspace = true
+lightning.workspace = true
 lightning-invoice.workspace = true
 regex.workspace = true
 reqwest = { workspace = true, optional = true }

+ 23 - 25
crates/cdk/src/mint/builder.rs

@@ -210,32 +210,30 @@ impl MintBuilder {
             self.mint_info.nuts.nut15 = mpp;
         }
 
-        if method == PaymentMethod::Bolt11 {
-            let mint_method_settings = MintMethodSettings {
-                method: method.clone(),
-                unit: unit.clone(),
-                min_amount: Some(limits.mint_min),
-                max_amount: Some(limits.mint_max),
-                options: Some(MintMethodOptions::Bolt11 {
-                    description: settings.invoice_description,
-                }),
-            };
+        let mint_method_settings = MintMethodSettings {
+            method: method.clone(),
+            unit: unit.clone(),
+            min_amount: Some(limits.mint_min),
+            max_amount: Some(limits.mint_max),
+            options: Some(MintMethodOptions::Bolt11 {
+                description: settings.invoice_description,
+            }),
+        };
 
-            self.mint_info.nuts.nut04.methods.push(mint_method_settings);
-            self.mint_info.nuts.nut04.disabled = false;
-
-            let melt_method_settings = MeltMethodSettings {
-                method,
-                unit,
-                min_amount: Some(limits.melt_min),
-                max_amount: Some(limits.melt_max),
-                options: Some(MeltMethodOptions::Bolt11 {
-                    amountless: settings.amountless,
-                }),
-            };
-            self.mint_info.nuts.nut05.methods.push(melt_method_settings);
-            self.mint_info.nuts.nut05.disabled = false;
-        }
+        self.mint_info.nuts.nut04.methods.push(mint_method_settings);
+        self.mint_info.nuts.nut04.disabled = false;
+
+        let melt_method_settings = MeltMethodSettings {
+            method,
+            unit,
+            min_amount: Some(limits.melt_min),
+            max_amount: Some(limits.melt_max),
+            options: Some(MeltMethodOptions::Bolt11 {
+                amountless: settings.amountless,
+            }),
+        };
+        self.mint_info.nuts.nut05.methods.push(melt_method_settings);
+        self.mint_info.nuts.nut05.disabled = false;
 
         ln.insert(ln_key.clone(), ln_backend);
 

+ 0 - 301
crates/cdk/src/mint/issue/issue_nut04.rs

@@ -1,301 +0,0 @@
-use cdk_common::payment::Bolt11Settings;
-use tracing::instrument;
-use uuid::Uuid;
-
-use crate::mint::{
-    CurrencyUnit, MintQuote, MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteState,
-    MintRequest, MintResponse, NotificationPayload, PublicKey, Verification,
-};
-use crate::nuts::PaymentMethod;
-use crate::util::unix_time;
-use crate::{ensure_cdk, Amount, Error, Mint};
-
-impl Mint {
-    /// Checks that minting is enabled, request is supported unit and within range
-    async fn check_mint_request_acceptable(
-        &self,
-        amount: Amount,
-        unit: &CurrencyUnit,
-    ) -> Result<(), Error> {
-        let mint_info = self.localstore.get_mint_info().await?;
-        let nut04 = &mint_info.nuts.nut04;
-
-        ensure_cdk!(!nut04.disabled, Error::MintingDisabled);
-
-        let settings = nut04
-            .get_settings(unit, &PaymentMethod::Bolt11)
-            .ok_or(Error::UnsupportedUnit)?;
-
-        let is_above_max = settings
-            .max_amount
-            .is_some_and(|max_amount| amount > max_amount);
-        let is_below_min = settings
-            .min_amount
-            .is_some_and(|min_amount| amount < min_amount);
-        let is_out_of_range = is_above_max || is_below_min;
-
-        ensure_cdk!(
-            !is_out_of_range,
-            Error::AmountOutofLimitRange(
-                settings.min_amount.unwrap_or_default(),
-                settings.max_amount.unwrap_or_default(),
-                amount,
-            )
-        );
-
-        Ok(())
-    }
-
-    /// Create new mint bolt11 quote
-    #[instrument(skip_all)]
-    pub async fn get_mint_bolt11_quote(
-        &self,
-        mint_quote_request: MintQuoteBolt11Request,
-    ) -> Result<MintQuoteBolt11Response<Uuid>, Error> {
-        let MintQuoteBolt11Request {
-            amount,
-            unit,
-            description,
-            pubkey,
-        } = mint_quote_request;
-
-        self.check_mint_request_acceptable(amount, &unit).await?;
-
-        let ln = self.get_payment_processor(unit.clone(), PaymentMethod::Bolt11)?;
-
-        let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
-
-        let quote_expiry = unix_time() + mint_ttl;
-
-        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_incoming_payment_request(
-                amount,
-                &unit,
-                description.unwrap_or("".to_string()),
-                Some(quote_expiry),
-            )
-            .await
-            .map_err(|err| {
-                tracing::error!("Could not create invoice: {}", err);
-                Error::InvalidPaymentRequest
-            })?;
-
-        let quote = MintQuote::new(
-            create_invoice_response.request.to_string(),
-            unit.clone(),
-            amount,
-            create_invoice_response.expiry.unwrap_or(0),
-            create_invoice_response.request_lookup_id.clone(),
-            pubkey,
-        );
-
-        tracing::debug!(
-            "New mint quote {} for {} {} with request id {}",
-            quote.id,
-            amount,
-            unit,
-            create_invoice_response.request_lookup_id,
-        );
-
-        let mut tx = self.localstore.begin_transaction().await?;
-        tx.add_or_replace_mint_quote(quote.clone()).await?;
-        tx.commit().await?;
-
-        let quote: MintQuoteBolt11Response<Uuid> = quote.into();
-
-        self.pubsub_manager
-            .broadcast(NotificationPayload::MintQuoteBolt11Response(quote.clone()));
-
-        Ok(quote)
-    }
-
-    /// Check mint quote
-    #[instrument(skip(self))]
-    pub async fn check_mint_quote(
-        &self,
-        quote_id: &Uuid,
-    ) -> Result<MintQuoteBolt11Response<Uuid>, Error> {
-        let mut tx = self.localstore.begin_transaction().await?;
-        let mut mint_quote = tx
-            .get_mint_quote(quote_id)
-            .await?
-            .ok_or(Error::UnknownQuote)?;
-
-        // Since the pending state is not part of the NUT it should not be part of the
-        // response. In practice the wallet should not be checking the state of
-        // a quote while waiting for the mint response.
-        if mint_quote.state == MintQuoteState::Unpaid {
-            self.check_mint_quote_paid(tx, &mut mint_quote)
-                .await?
-                .commit()
-                .await?;
-        }
-
-        Ok(MintQuoteBolt11Response {
-            quote: mint_quote.id,
-            request: mint_quote.request,
-            state: mint_quote.state,
-            expiry: Some(mint_quote.expiry),
-            pubkey: mint_quote.pubkey,
-            amount: Some(mint_quote.amount),
-            unit: Some(mint_quote.unit.clone()),
-        })
-    }
-
-    /// Get mint quotes
-    #[instrument(skip_all)]
-    pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
-        let quotes = self.localstore.get_mint_quotes().await?;
-        Ok(quotes)
-    }
-
-    /// Remove mint quote
-    #[instrument(skip_all)]
-    pub async fn remove_mint_quote(&self, quote_id: &Uuid) -> Result<(), Error> {
-        let mut tx = self.localstore.begin_transaction().await?;
-        tx.remove_mint_quote(quote_id).await?;
-        tx.commit().await?;
-
-        Ok(())
-    }
-
-    /// Flag mint quote as paid
-    #[instrument(skip_all)]
-    pub async fn pay_mint_quote_for_request_id(
-        &self,
-        request_lookup_id: &str,
-    ) -> Result<(), Error> {
-        if let Ok(Some(mint_quote)) = self
-            .localstore
-            .get_mint_quote_by_request_lookup_id(request_lookup_id)
-            .await
-        {
-            self.pay_mint_quote(&mint_quote).await?;
-        }
-        Ok(())
-    }
-
-    /// Mark mint quote as paid
-    #[instrument(skip_all)]
-    pub async fn pay_mint_quote(&self, mint_quote: &MintQuote) -> Result<(), Error> {
-        tracing::debug!(
-            "Received payment notification for mint quote {}",
-            mint_quote.id
-        );
-        if mint_quote.state != MintQuoteState::Issued && mint_quote.state != MintQuoteState::Paid {
-            let mut tx = self.localstore.begin_transaction().await?;
-            tx.update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid)
-                .await?;
-            tx.commit().await?;
-        } else {
-            tracing::debug!(
-                "{} Quote already {} continuing",
-                mint_quote.id,
-                mint_quote.state
-            );
-        }
-
-        self.pubsub_manager
-            .mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Paid);
-
-        Ok(())
-    }
-
-    /// Process mint request
-    #[instrument(skip_all)]
-    pub async fn process_mint_request(
-        &self,
-        mint_request: MintRequest<Uuid>,
-    ) -> Result<MintResponse, Error> {
-        let mut tx = self.localstore.begin_transaction().await?;
-
-        let mut mint_quote = tx
-            .get_mint_quote(&mint_request.quote)
-            .await?
-            .ok_or(Error::UnknownQuote)?;
-
-        let mut tx = if mint_quote.state == MintQuoteState::Unpaid {
-            self.check_mint_quote_paid(tx, &mut mint_quote).await?
-        } else {
-            tx
-        };
-
-        match mint_quote.state {
-            MintQuoteState::Unpaid => {
-                return Err(Error::UnpaidQuote);
-            }
-            MintQuoteState::Pending => {
-                return Err(Error::PendingQuote);
-            }
-            MintQuoteState::Issued => {
-                return Err(Error::IssuedQuote);
-            }
-            MintQuoteState::Paid => (),
-        }
-
-        // If the there is a public key provoided in mint quote request
-        // verify the signature is provided for the mint request
-        if let Some(pubkey) = mint_quote.pubkey {
-            mint_request.verify_signature(pubkey)?;
-        }
-
-        let Verification { amount, unit } =
-            match self.verify_outputs(&mut tx, &mint_request.outputs).await {
-                Ok(verification) => verification,
-                Err(err) => {
-                    tracing::debug!("Could not verify mint outputs");
-                    return Err(err);
-                }
-            };
-
-        // We check the total value of blinded messages == mint quote
-        if amount != mint_quote.amount {
-            return Err(Error::TransactionUnbalanced(
-                mint_quote.amount.into(),
-                mint_request.total_amount()?.into(),
-                0,
-            ));
-        }
-
-        let unit = unit.ok_or(Error::UnsupportedUnit)?;
-        ensure_cdk!(unit == mint_quote.unit, Error::UnsupportedUnit);
-
-        let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
-
-        for blinded_message in mint_request.outputs.iter() {
-            let blind_signature = self.blind_sign(blinded_message.clone()).await?;
-            blind_signatures.push(blind_signature);
-        }
-
-        tx.add_blind_signatures(
-            &mint_request
-                .outputs
-                .iter()
-                .map(|p| p.blinded_secret)
-                .collect::<Vec<PublicKey>>(),
-            &blind_signatures,
-            Some(mint_request.quote),
-        )
-        .await?;
-
-        tx.update_mint_quote_state(&mint_request.quote, MintQuoteState::Issued)
-            .await?;
-
-        tx.commit().await?;
-
-        self.pubsub_manager
-            .mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued);
-
-        Ok(MintResponse {
-            signatures: blind_signatures,
-        })
-    }
-}

+ 599 - 1
crates/cdk/src/mint/issue/mod.rs

@@ -1,3 +1,601 @@
+use cdk_common::mint::MintQuote;
+use cdk_common::payment::{
+    Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
+    IncomingPaymentOptions, WaitPaymentResponse,
+};
+use cdk_common::util::unix_time;
+use cdk_common::{
+    database, ensure_cdk, Amount, CurrencyUnit, Error, MintQuoteBolt11Request,
+    MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteState,
+    MintRequest, MintResponse, NotificationPayload, PaymentMethod, PublicKey,
+};
+use tracing::instrument;
+use uuid::Uuid;
+
+use crate::mint::Verification;
+use crate::Mint;
+
 #[cfg(feature = "auth")]
 mod auth;
-mod issue_nut04;
+
+/// Request for creating a mint quote
+///
+/// This enum represents the different types of payment requests that can be used
+/// to create a mint quote.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MintQuoteRequest {
+    /// Lightning Network BOLT11 invoice request
+    Bolt11(MintQuoteBolt11Request),
+    /// Lightning Network BOLT12 offer request
+    Bolt12(MintQuoteBolt12Request),
+}
+
+impl From<MintQuoteBolt11Request> for MintQuoteRequest {
+    fn from(request: MintQuoteBolt11Request) -> Self {
+        MintQuoteRequest::Bolt11(request)
+    }
+}
+
+impl From<MintQuoteBolt12Request> for MintQuoteRequest {
+    fn from(request: MintQuoteBolt12Request) -> Self {
+        MintQuoteRequest::Bolt12(request)
+    }
+}
+
+/// Response for a mint quote request
+///
+/// This enum represents the different types of payment responses that can be returned
+/// when creating a mint quote.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MintQuoteResponse {
+    /// Lightning Network BOLT11 invoice response
+    Bolt11(MintQuoteBolt11Response<Uuid>),
+    /// Lightning Network BOLT12 offer response
+    Bolt12(MintQuoteBolt12Response<Uuid>),
+}
+
+impl TryFrom<MintQuoteResponse> for MintQuoteBolt11Response<Uuid> {
+    type Error = Error;
+
+    fn try_from(response: MintQuoteResponse) -> Result<Self, Self::Error> {
+        match response {
+            MintQuoteResponse::Bolt11(bolt11_response) => Ok(bolt11_response),
+            _ => Err(Error::InvalidPaymentMethod),
+        }
+    }
+}
+
+impl TryFrom<MintQuoteResponse> for MintQuoteBolt12Response<Uuid> {
+    type Error = Error;
+
+    fn try_from(response: MintQuoteResponse) -> Result<Self, Self::Error> {
+        match response {
+            MintQuoteResponse::Bolt12(bolt12_response) => Ok(bolt12_response),
+            _ => Err(Error::InvalidPaymentMethod),
+        }
+    }
+}
+
+impl TryFrom<MintQuote> for MintQuoteResponse {
+    type Error = Error;
+
+    fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
+        match quote.payment_method {
+            PaymentMethod::Bolt11 => {
+                let bolt11_response: MintQuoteBolt11Response<Uuid> = quote.into();
+                Ok(MintQuoteResponse::Bolt11(bolt11_response))
+            }
+            PaymentMethod::Bolt12 => {
+                let bolt12_response = MintQuoteBolt12Response::try_from(quote)?;
+                Ok(MintQuoteResponse::Bolt12(bolt12_response))
+            }
+            PaymentMethod::Custom(_) => Err(Error::InvalidPaymentMethod),
+        }
+    }
+}
+
+impl From<MintQuoteResponse> for MintQuoteBolt11Response<String> {
+    fn from(response: MintQuoteResponse) -> Self {
+        match response {
+            MintQuoteResponse::Bolt11(bolt11_response) => MintQuoteBolt11Response {
+                quote: bolt11_response.quote.to_string(),
+                state: bolt11_response.state,
+                request: bolt11_response.request,
+                expiry: bolt11_response.expiry,
+                pubkey: bolt11_response.pubkey,
+                amount: bolt11_response.amount,
+                unit: bolt11_response.unit,
+            },
+            _ => panic!("Expected Bolt11 response"),
+        }
+    }
+}
+
+impl Mint {
+    /// Validates that a mint request meets all requirements
+    ///
+    /// Checks that:
+    /// - Minting is enabled for the requested payment method
+    /// - The currency unit is supported
+    /// - The amount (if provided) is within the allowed range for the payment method
+    ///
+    /// # Arguments
+    /// * `amount` - Optional amount to validate
+    /// * `unit` - Currency unit for the request
+    /// * `payment_method` - Payment method (Bolt11, Bolt12, etc.)
+    ///
+    /// # Returns
+    /// * `Ok(())` if the request is acceptable
+    /// * `Error` if any validation fails
+    pub async fn check_mint_request_acceptable(
+        &self,
+        amount: Option<Amount>,
+        unit: &CurrencyUnit,
+        payment_method: &PaymentMethod,
+    ) -> Result<(), Error> {
+        let mint_info = self.localstore.get_mint_info().await?;
+
+        let nut04 = &mint_info.nuts.nut04;
+        ensure_cdk!(!nut04.disabled, Error::MintingDisabled);
+
+        let disabled = nut04.disabled;
+
+        ensure_cdk!(!disabled, Error::MintingDisabled);
+
+        let settings = nut04
+            .get_settings(unit, payment_method)
+            .ok_or(Error::UnsupportedUnit)?;
+
+        let min_amount = settings.min_amount;
+        let max_amount = settings.max_amount;
+
+        // Check amount limits if an amount is provided
+        if let Some(amount) = amount {
+            let is_above_max = max_amount.is_some_and(|max_amount| amount > max_amount);
+            let is_below_min = min_amount.is_some_and(|min_amount| amount < min_amount);
+            let is_out_of_range = is_above_max || is_below_min;
+
+            ensure_cdk!(
+                !is_out_of_range,
+                Error::AmountOutofLimitRange(
+                    min_amount.unwrap_or_default(),
+                    max_amount.unwrap_or_default(),
+                    amount,
+                )
+            );
+        }
+
+        Ok(())
+    }
+
+    /// Creates a new mint quote for the specified payment request
+    ///
+    /// Handles both Bolt11 and Bolt12 payment requests by:
+    /// 1. Validating the request parameters
+    /// 2. Creating an appropriate payment request via the payment processor
+    /// 3. Storing the quote in the database
+    /// 4. Broadcasting a notification about the new quote
+    ///
+    /// # Arguments
+    /// * `mint_quote_request` - The request containing payment details
+    ///
+    /// # Returns
+    /// * `MintQuoteResponse` - Response with payment details if successful
+    /// * `Error` - If the request is invalid or payment creation fails
+    #[instrument(skip_all)]
+    pub async fn get_mint_quote(
+        &self,
+        mint_quote_request: MintQuoteRequest,
+    ) -> Result<MintQuoteResponse, Error> {
+        let unit: CurrencyUnit;
+        let amount;
+        let pubkey;
+        let payment_method;
+
+        let create_invoice_response = match mint_quote_request {
+            MintQuoteRequest::Bolt11(bolt11_request) => {
+                unit = bolt11_request.unit;
+                amount = Some(bolt11_request.amount);
+                pubkey = bolt11_request.pubkey;
+                payment_method = PaymentMethod::Bolt11;
+
+                self.check_mint_request_acceptable(
+                    Some(bolt11_request.amount),
+                    &unit,
+                    &payment_method,
+                )
+                .await?;
+
+                let ln = self.get_payment_processor(unit.clone(), PaymentMethod::Bolt11)?;
+
+                let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
+
+                let quote_expiry = unix_time() + mint_ttl;
+
+                let settings = ln.get_settings().await?;
+                let settings: Bolt11Settings = serde_json::from_value(settings)?;
+
+                let description = bolt11_request.description;
+
+                if description.is_some() && !settings.invoice_description {
+                    tracing::error!("Backend does not support invoice description");
+                    return Err(Error::InvoiceDescriptionUnsupported);
+                }
+
+                let bolt11_options = Bolt11IncomingPaymentOptions {
+                    description,
+                    amount: bolt11_request.amount,
+                    unix_expiry: Some(quote_expiry),
+                };
+
+                let incoming_options = IncomingPaymentOptions::Bolt11(bolt11_options);
+
+                ln.create_incoming_payment_request(&unit, incoming_options)
+                    .await
+                    .map_err(|err| {
+                        tracing::error!("Could not create invoice: {}", err);
+                        Error::InvalidPaymentRequest
+                    })?
+            }
+            MintQuoteRequest::Bolt12(bolt12_request) => {
+                unit = bolt12_request.unit;
+                amount = bolt12_request.amount;
+                pubkey = Some(bolt12_request.pubkey);
+                payment_method = PaymentMethod::Bolt12;
+
+                self.check_mint_request_acceptable(amount, &unit, &payment_method)
+                    .await?;
+
+                let ln = self.get_payment_processor(unit.clone(), payment_method.clone())?;
+
+                let description = bolt12_request.description;
+
+                let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
+
+                let expiry = unix_time() + mint_ttl;
+
+                let bolt12_options = Bolt12IncomingPaymentOptions {
+                    description,
+                    amount,
+                    unix_expiry: Some(expiry),
+                };
+
+                let incoming_options = IncomingPaymentOptions::Bolt12(Box::new(bolt12_options));
+
+                ln.create_incoming_payment_request(&unit, incoming_options)
+                    .await
+                    .map_err(|err| {
+                        tracing::error!("Could not create invoice: {}", err);
+                        Error::InvalidPaymentRequest
+                    })?
+            }
+        };
+
+        let quote = MintQuote::new(
+            None,
+            create_invoice_response.request.to_string(),
+            unit.clone(),
+            amount,
+            create_invoice_response.expiry.unwrap_or(0),
+            create_invoice_response.request_lookup_id.clone(),
+            pubkey,
+            Amount::ZERO,
+            Amount::ZERO,
+            payment_method.clone(),
+            unix_time(),
+            vec![],
+            vec![],
+        );
+
+        tracing::debug!(
+            "New {} mint quote {} for {:?} {} with request id {:?}",
+            payment_method,
+            quote.id,
+            amount,
+            unit,
+            create_invoice_response.request_lookup_id,
+        );
+
+        let mut tx = self.localstore.begin_transaction().await?;
+        tx.add_mint_quote(quote.clone()).await?;
+        tx.commit().await?;
+
+        match payment_method {
+            PaymentMethod::Bolt11 => {
+                let res: MintQuoteBolt11Response<Uuid> = quote.clone().into();
+                self.pubsub_manager
+                    .broadcast(NotificationPayload::MintQuoteBolt11Response(res));
+            }
+            PaymentMethod::Bolt12 => {
+                let res: MintQuoteBolt12Response<Uuid> = quote.clone().try_into()?;
+                self.pubsub_manager
+                    .broadcast(NotificationPayload::MintQuoteBolt12Response(res));
+            }
+            PaymentMethod::Custom(_) => {}
+        }
+
+        quote.try_into()
+    }
+
+    /// Retrieves all mint quotes from the database
+    ///
+    /// # Returns
+    /// * `Vec<MintQuote>` - List of all mint quotes
+    /// * `Error` if database access fails
+    #[instrument(skip_all)]
+    pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let quotes = self.localstore.get_mint_quotes().await?;
+        Ok(quotes)
+    }
+
+    /// Removes a mint quote from the database
+    ///
+    /// # Arguments
+    /// * `quote_id` - The UUID of the quote to remove
+    ///
+    /// # Returns
+    /// * `Ok(())` if removal was successful
+    /// * `Error` if the quote doesn't exist or removal fails
+    #[instrument(skip_all)]
+    pub async fn remove_mint_quote(&self, quote_id: &Uuid) -> Result<(), Error> {
+        let mut tx = self.localstore.begin_transaction().await?;
+        tx.remove_mint_quote(quote_id).await?;
+        tx.commit().await?;
+
+        Ok(())
+    }
+
+    /// Marks a mint quote as paid based on the payment request ID
+    ///
+    /// Looks up the mint quote by the payment request ID and marks it as paid
+    /// if found.
+    ///
+    /// # Arguments
+    /// * `wait_payment_response` - Payment response containing payment details
+    ///
+    /// # Returns
+    /// * `Ok(())` if the quote was found and updated
+    /// * `Error` if the update fails
+    #[instrument(skip_all)]
+    pub async fn pay_mint_quote_for_request_id(
+        &self,
+        wait_payment_response: WaitPaymentResponse,
+    ) -> Result<(), Error> {
+        if wait_payment_response.payment_amount == Amount::ZERO {
+            tracing::warn!(
+                "Received payment response with 0 amount with payment id {}.",
+                wait_payment_response.payment_id
+            );
+
+            return Err(Error::AmountUndefined);
+        }
+
+        let mut tx = self.localstore.begin_transaction().await?;
+
+        if let Ok(Some(mint_quote)) = tx
+            .get_mint_quote_by_request_lookup_id(&wait_payment_response.payment_identifier)
+            .await
+        {
+            self.pay_mint_quote(&mut tx, &mint_quote, wait_payment_response)
+                .await?;
+        } else {
+            tracing::warn!(
+                "Could not get request for request lookup id {:?}.",
+                wait_payment_response.payment_identifier
+            );
+        }
+
+        tx.commit().await?;
+
+        Ok(())
+    }
+
+    /// Marks a specific mint quote as paid
+    ///
+    /// Updates the mint quote with payment information and broadcasts
+    /// a notification about the payment status change.
+    ///
+    /// # Arguments
+    /// * `mint_quote` - The mint quote to mark as paid
+    /// * `wait_payment_response` - Payment response containing payment details
+    ///
+    /// # Returns
+    /// * `Ok(())` if the update was successful
+    /// * `Error` if the update fails
+    #[instrument(skip_all)]
+    pub async fn pay_mint_quote(
+        &self,
+        tx: &mut Box<dyn database::MintTransaction<'_, database::Error> + Send + Sync + '_>,
+        mint_quote: &MintQuote,
+        wait_payment_response: WaitPaymentResponse,
+    ) -> Result<(), Error> {
+        tracing::debug!(
+            "Received payment notification of {} for mint quote {} with payment id {}",
+            wait_payment_response.payment_amount,
+            mint_quote.id,
+            wait_payment_response.payment_id
+        );
+
+        let quote_state = mint_quote.state();
+        if !mint_quote
+            .payment_ids()
+            .contains(&&wait_payment_response.payment_id)
+        {
+            if mint_quote.payment_method == PaymentMethod::Bolt11
+                && (quote_state == MintQuoteState::Issued || quote_state == MintQuoteState::Paid)
+            {
+                tracing::info!("Received payment notification for already seen payment.");
+            } else {
+                tx.increment_mint_quote_amount_paid(
+                    &mint_quote.id,
+                    wait_payment_response.payment_amount,
+                    wait_payment_response.payment_id,
+                )
+                .await?;
+
+                self.pubsub_manager
+                    .mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Paid);
+            }
+        } else {
+            tracing::info!("Received payment notification for already seen payment.");
+        }
+
+        Ok(())
+    }
+
+    /// Checks the status of a mint quote and updates it if necessary
+    ///
+    /// If the quote is unpaid, this will check if payment has been received.
+    /// Returns the current state of the quote.
+    ///
+    /// # Arguments
+    /// * `quote_id` - The UUID of the quote to check
+    ///
+    /// # Returns
+    /// * `MintQuoteResponse` - The current state of the quote
+    /// * `Error` if the quote doesn't exist or checking fails
+    #[instrument(skip(self))]
+    pub async fn check_mint_quote(&self, quote_id: &Uuid) -> Result<MintQuoteResponse, Error> {
+        let mut quote = self
+            .localstore
+            .get_mint_quote(quote_id)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        self.check_mint_quote_paid(&mut quote).await?;
+
+        quote.try_into()
+    }
+
+    /// Processes a mint request to issue new tokens
+    ///
+    /// This function:
+    /// 1. Verifies the mint quote exists and is paid
+    /// 2. Validates the request signature if a pubkey was provided
+    /// 3. Verifies the outputs match the expected amount
+    /// 4. Signs the blinded messages
+    /// 5. Updates the quote status
+    /// 6. Broadcasts a notification about the status change
+    ///
+    /// # Arguments
+    /// * `mint_request` - The mint request containing blinded outputs to sign
+    ///
+    /// # Returns
+    /// * `MintBolt11Response` - Response containing blind signatures
+    /// * `Error` if validation fails or signing fails
+    #[instrument(skip_all)]
+    pub async fn process_mint_request(
+        &self,
+        mint_request: MintRequest<Uuid>,
+    ) -> Result<MintResponse, Error> {
+        let mut mint_quote = self
+            .localstore
+            .get_mint_quote(&mint_request.quote)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        self.check_mint_quote_paid(&mut mint_quote).await?;
+
+        let mut tx = self.localstore.begin_transaction().await?;
+
+        let mint_quote = tx
+            .get_mint_quote(&mint_request.quote)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        match mint_quote.state() {
+            MintQuoteState::Unpaid => {
+                return Err(Error::UnpaidQuote);
+            }
+            MintQuoteState::Issued => {
+                if mint_quote.payment_method == PaymentMethod::Bolt12
+                    && mint_quote.amount_paid() > mint_quote.amount_issued()
+                {
+                    tracing::warn!("Mint quote should state should have been set to issued upon new payment. Something isn't right. Stopping mint");
+                }
+
+                return Err(Error::IssuedQuote);
+            }
+            MintQuoteState::Paid => (),
+        }
+
+        if mint_quote.payment_method == PaymentMethod::Bolt12 && mint_quote.pubkey.is_none() {
+            tracing::warn!("Bolt12 mint quote created without pubkey");
+            return Err(Error::SignatureMissingOrInvalid);
+        }
+
+        let mint_amount = match mint_quote.payment_method {
+            PaymentMethod::Bolt11 => mint_quote.amount.ok_or(Error::AmountUndefined)?,
+            PaymentMethod::Bolt12 => {
+                if mint_quote.amount_issued() > mint_quote.amount_paid() {
+                    tracing::error!(
+                        "Quote state should not be issued if issued {} is > paid {}.",
+                        mint_quote.amount_issued(),
+                        mint_quote.amount_paid()
+                    );
+                    return Err(Error::UnpaidQuote);
+                }
+                mint_quote.amount_paid() - mint_quote.amount_issued()
+            }
+            _ => return Err(Error::UnsupportedPaymentMethod),
+        };
+
+        // If the there is a public key provoided in mint quote request
+        // verify the signature is provided for the mint request
+        if let Some(pubkey) = mint_quote.pubkey {
+            mint_request.verify_signature(pubkey)?;
+        }
+
+        let Verification { amount, unit } =
+            match self.verify_outputs(&mut tx, &mint_request.outputs).await {
+                Ok(verification) => verification,
+                Err(err) => {
+                    tracing::debug!("Could not verify mint outputs");
+
+                    return Err(err);
+                }
+            };
+
+        // We check the total value of blinded messages == mint quote
+        if amount != mint_amount {
+            return Err(Error::TransactionUnbalanced(
+                mint_amount.into(),
+                mint_request.total_amount()?.into(),
+                0,
+            ));
+        }
+
+        let unit = unit.ok_or(Error::UnsupportedUnit).unwrap();
+        ensure_cdk!(unit == mint_quote.unit, Error::UnsupportedUnit);
+
+        let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
+
+        for blinded_message in mint_request.outputs.iter() {
+            let blind_signature = self.blind_sign(blinded_message.clone()).await?;
+            blind_signatures.push(blind_signature);
+        }
+
+        tx.add_blind_signatures(
+            &mint_request
+                .outputs
+                .iter()
+                .map(|p| p.blinded_secret)
+                .collect::<Vec<PublicKey>>(),
+            &blind_signatures,
+            Some(mint_request.quote),
+        )
+        .await?;
+
+        tx.increment_mint_quote_amount_issued(&mint_request.quote, mint_request.total_amount()?)
+            .await?;
+
+        tx.commit().await?;
+
+        self.pubsub_manager
+            .mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued);
+
+        Ok(MintResponse {
+            signatures: blind_signatures,
+        })
+    }
+}

+ 33 - 16
crates/cdk/src/mint/ln.rs

@@ -1,21 +1,31 @@
+use cdk_common::amount::to_unit;
 use cdk_common::common::PaymentProcessorKey;
-use cdk_common::database::{self, MintTransaction};
 use cdk_common::mint::MintQuote;
-use cdk_common::MintQuoteState;
+use cdk_common::util::unix_time;
+use cdk_common::{MintQuoteState, PaymentMethod};
+use tracing::instrument;
 
 use super::Mint;
 use crate::Error;
 
 impl Mint {
     /// Check the status of an ln payment for a quote
-    pub async fn check_mint_quote_paid(
-        &self,
-        tx: Box<dyn MintTransaction<'_, database::Error> + Send + Sync + '_>,
-        quote: &mut MintQuote,
-    ) -> Result<Box<dyn MintTransaction<'_, database::Error> + Send + Sync + '_>, Error> {
+    #[instrument(skip_all)]
+    pub async fn check_mint_quote_paid(&self, quote: &mut MintQuote) -> Result<(), Error> {
+        let state = quote.state();
+
+        // We can just return here and do not need to check with ln node.
+        // If quote is issued it is already in a final state,
+        // If it is paid ln node will only tell us what we already know
+        if quote.payment_method == PaymentMethod::Bolt11
+            && (state == MintQuoteState::Issued || state == MintQuoteState::Paid)
+        {
+            return Ok(());
+        }
+
         let ln = match self.ln.get(&PaymentProcessorKey::new(
             quote.unit.clone(),
-            cdk_common::PaymentMethod::Bolt11,
+            quote.payment_method.clone(),
         )) {
             Some(ln) => ln,
             None => {
@@ -25,23 +35,30 @@ impl Mint {
             }
         };
 
-        tx.commit().await?;
-
         let ln_status = ln
             .check_incoming_payment_status(&quote.request_lookup_id)
             .await?;
 
         let mut tx = self.localstore.begin_transaction().await?;
 
-        if ln_status != quote.state && quote.state != MintQuoteState::Issued {
-            tx.update_mint_quote_state(&quote.id, ln_status).await?;
+        for payment in ln_status {
+            if !quote.payment_ids().contains(&&payment.payment_id) {
+                tracing::debug!("Found payment for quote {} when checking.", quote.id);
+                let amount_paid = to_unit(payment.payment_amount, &payment.unit, &quote.unit)?;
 
-            quote.state = ln_status;
+                quote.increment_amount_paid(amount_paid)?;
+                quote.add_payment(amount_paid, payment.payment_id.clone(), unix_time())?;
 
-            self.pubsub_manager
-                .mint_quote_bolt11_status(quote.clone(), ln_status);
+                tx.increment_mint_quote_amount_paid(&quote.id, amount_paid, payment.payment_id)
+                    .await?;
+
+                self.pubsub_manager
+                    .mint_quote_bolt11_status(quote.clone(), MintQuoteState::Paid);
+            }
         }
 
-        Ok(tx)
+        tx.commit().await?;
+
+        Ok(())
     }
 }

+ 210 - 60
crates/cdk/src/mint/melt.rs

@@ -1,11 +1,18 @@
 use std::str::FromStr;
 
 use anyhow::bail;
+use cdk_common::amount::amount_for_offer;
 use cdk_common::database::{self, MintTransaction};
+use cdk_common::melt::MeltQuoteRequest;
+use cdk_common::mint::MeltPaymentRequest;
 use cdk_common::nut00::ProofsMethods;
 use cdk_common::nut05::MeltMethodOptions;
-use cdk_common::MeltOptions;
-use lightning_invoice::Bolt11Invoice;
+use cdk_common::payment::{
+    Bolt11OutgoingPaymentOptions, Bolt12OutgoingPaymentOptions, OutgoingPaymentOptions,
+    PaymentQuoteOptions,
+};
+use cdk_common::{MeltOptions, MeltQuoteBolt12Request};
+use lightning::offers::offer::Offer;
 use tracing::instrument;
 use uuid::Uuid;
 
@@ -65,10 +72,12 @@ impl Mint {
                 amount
             }
             Some(MeltOptions::Amountless { amountless: _ }) => {
-                if !matches!(
-                    settings.options,
-                    Some(MeltMethodOptions::Bolt11 { amountless: true })
-                ) {
+                if method == PaymentMethod::Bolt11
+                    && !matches!(
+                        settings.options,
+                        Some(MeltMethodOptions::Bolt11 { amountless: true })
+                    )
+                {
                     return Err(Error::AmountlessInvoiceNotSupported(unit, method));
                 }
 
@@ -97,9 +106,28 @@ impl Mint {
         }
     }
 
-    /// Get melt bolt11 quote
+    /// Get melt quote for either BOLT11 or BOLT12
+    ///
+    /// This function accepts a `MeltQuoteRequest` enum and delegates to the
+    /// appropriate handler based on the request type.
+    #[instrument(skip_all)]
+    pub async fn get_melt_quote(
+        &self,
+        melt_quote_request: MeltQuoteRequest,
+    ) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
+        match melt_quote_request {
+            MeltQuoteRequest::Bolt11(bolt11_request) => {
+                self.get_melt_bolt11_quote_impl(&bolt11_request).await
+            }
+            MeltQuoteRequest::Bolt12(bolt12_request) => {
+                self.get_melt_bolt12_quote_impl(&bolt12_request).await
+            }
+        }
+    }
+
+    /// Implementation of get_melt_bolt11_quote
     #[instrument(skip_all)]
-    pub async fn get_melt_bolt11_quote(
+    async fn get_melt_bolt11_quote_impl(
         &self,
         melt_request: &MeltQuoteBolt11Request,
     ) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
@@ -110,6 +138,19 @@ impl Mint {
             ..
         } = melt_request;
 
+        let amount_msats = melt_request.amount_msat()?;
+
+        let amount_quote_unit = to_unit(amount_msats, &CurrencyUnit::Msat, unit)?;
+
+        self.check_melt_request_acceptable(
+            amount_quote_unit,
+            unit.clone(),
+            PaymentMethod::Bolt11,
+            request.to_string(),
+            *options,
+        )
+        .await?;
+
         let ln = self
             .ln
             .get(&PaymentProcessorKey::new(
@@ -122,11 +163,17 @@ impl Mint {
                 Error::UnsupportedUnit
             })?;
 
+        let bolt11 = Bolt11OutgoingPaymentOptions {
+            bolt11: melt_request.request.clone(),
+            max_fee_amount: None,
+            timeout_secs: None,
+            melt_options: melt_request.options,
+        };
+
         let payment_quote = ln
             .get_payment_quote(
-                &melt_request.request.to_string(),
                 &melt_request.unit,
-                melt_request.options,
+                OutgoingPaymentOptions::Bolt11(Box::new(bolt11)),
             )
             .await
             .map_err(|err| {
@@ -139,62 +186,137 @@ impl Mint {
                 Error::UnsupportedUnit
             })?;
 
-        self.check_melt_request_acceptable(
-            payment_quote.amount,
+        let melt_ttl = self.localstore.get_quote_ttl().await?.melt_ttl;
+
+        let quote = MeltQuote::new(
+            MeltPaymentRequest::Bolt11 {
+                bolt11: request.clone(),
+            },
             unit.clone(),
+            payment_quote.amount,
+            payment_quote.fee,
+            unix_time() + melt_ttl,
+            payment_quote.request_lookup_id.clone(),
+            *options,
             PaymentMethod::Bolt11,
-            request.to_string(),
+        );
+
+        tracing::debug!(
+            "New melt quote {} for {} {} with request id {}",
+            quote.id,
+            amount_quote_unit,
+            unit,
+            payment_quote.request_lookup_id
+        );
+
+        let mut tx = self.localstore.begin_transaction().await?;
+        tx.add_melt_quote(quote.clone()).await?;
+        tx.commit().await?;
+
+        Ok(quote.into())
+    }
+
+    /// Implementation of get_melt_bolt12_quote
+    #[instrument(skip_all)]
+    async fn get_melt_bolt12_quote_impl(
+        &self,
+        melt_request: &MeltQuoteBolt12Request,
+    ) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
+        let MeltQuoteBolt12Request {
+            request,
+            unit,
+            options,
+        } = melt_request;
+
+        let offer = Offer::from_str(request).map_err(|_| Error::InvalidPaymentRequest)?;
+
+        let amount = match options {
+            Some(options) => match options {
+                MeltOptions::Amountless { amountless } => {
+                    to_unit(amountless.amount_msat, &CurrencyUnit::Msat, unit)?
+                }
+                _ => return Err(Error::UnsupportedUnit),
+            },
+            None => amount_for_offer(&offer, unit).map_err(|_| Error::UnsupportedUnit)?,
+        };
+
+        self.check_melt_request_acceptable(
+            amount,
+            unit.clone(),
+            PaymentMethod::Bolt12,
+            request.clone(),
             *options,
         )
         .await?;
 
-        // 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
-        let msats_to_pay = options.map(|opt| opt.amount_msat());
+        let ln = self
+            .ln
+            .get(&PaymentProcessorKey::new(
+                unit.clone(),
+                PaymentMethod::Bolt12,
+            ))
+            .ok_or_else(|| {
+                tracing::info!("Could not get ln backend for {}, bolt12 ", unit);
+
+                Error::UnsupportedUnit
+            })?;
 
-        let melt_ttl = self.localstore.get_quote_ttl().await?.melt_ttl;
+        let offer = Offer::from_str(&melt_request.request).map_err(|_| Error::Bolt12parse)?;
+
+        let outgoing_payment_options = Bolt12OutgoingPaymentOptions {
+            offer: offer.clone(),
+            max_fee_amount: None,
+            timeout_secs: None,
+            melt_options: *options,
+            invoice: None,
+        };
+
+        let payment_quote = ln
+            .get_payment_quote(
+                &melt_request.unit,
+                OutgoingPaymentOptions::Bolt12(Box::new(outgoing_payment_options)),
+            )
+            .await
+            .map_err(|err| {
+                tracing::error!(
+                    "Could not get payment quote for mint quote, {} bolt12, {}",
+                    unit,
+                    err
+                );
+
+                Error::UnsupportedUnit
+            })?;
+
+        let invoice = payment_quote.options.and_then(|options| match options {
+            PaymentQuoteOptions::Bolt12 { invoice } => invoice,
+        });
+
+        let payment_request = MeltPaymentRequest::Bolt12 {
+            offer: Box::new(offer),
+            invoice,
+        };
 
         let quote = MeltQuote::new(
-            request.to_string(),
+            payment_request,
             unit.clone(),
             payment_quote.amount,
             payment_quote.fee,
-            unix_time() + melt_ttl,
+            unix_time() + self.quote_ttl().await?.melt_ttl,
             payment_quote.request_lookup_id.clone(),
-            msats_to_pay,
+            *options,
+            PaymentMethod::Bolt12,
         );
 
         tracing::debug!(
             "New melt quote {} for {} {} with request id {}",
             quote.id,
-            payment_quote.amount,
+            amount,
             unit,
             payment_quote.request_lookup_id
         );
 
         let mut tx = self.localstore.begin_transaction().await?;
-        if let Some(mut from_db_quote) = tx.get_melt_quote(&quote.id).await? {
-            if from_db_quote.state != quote.state {
-                tx.update_melt_quote_state(&quote.id, from_db_quote.state)
-                    .await?;
-                from_db_quote.state = quote.state;
-            }
-            if from_db_quote.request_lookup_id != quote.request_lookup_id {
-                tx.update_melt_quote_request_lookup_id(&quote.id, &quote.request_lookup_id)
-                    .await?;
-                from_db_quote.request_lookup_id = quote.request_lookup_id.clone();
-            }
-            if from_db_quote != quote {
-                return Err(Error::Internal);
-            }
-        } else if let Err(err) = tx.add_melt_quote(quote.clone()).await {
-            match err {
-                database::Error::Duplicate => {
-                    return Err(Error::RequestAlreadyPaid);
-                }
-                _ => return Err(Error::from(err)),
-            }
-        }
+        tx.add_melt_quote(quote.clone()).await?;
         tx.commit().await?;
 
         Ok(quote.into())
@@ -228,7 +350,7 @@ impl Mint {
             fee_reserve: quote.fee_reserve,
             payment_preimage: quote.payment_preimage,
             change,
-            request: Some(quote.request.clone()),
+            request: Some(quote.request.to_string()),
             unit: Some(quote.unit.clone()),
         })
     }
@@ -247,16 +369,40 @@ impl Mint {
         melt_quote: &MeltQuote,
         melt_request: &MeltRequest<Uuid>,
     ) -> Result<Option<Amount>, Error> {
-        let invoice = Bolt11Invoice::from_str(&melt_quote.request)?;
-
         let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
             .expect("Quote unit is checked above that it can convert to msat");
 
-        let invoice_amount_msats: Amount = match invoice.amount_milli_satoshis() {
-            Some(amt) => amt.into(),
-            None => melt_quote
-                .msat_to_pay
-                .ok_or(Error::InvoiceAmountUndefined)?,
+        let invoice_amount_msats = match &melt_quote.request {
+            MeltPaymentRequest::Bolt11 { bolt11 } => match bolt11.amount_milli_satoshis() {
+                Some(amount) => amount.into(),
+                None => melt_quote
+                    .options
+                    .ok_or(Error::InvoiceAmountUndefined)?
+                    .amount_msat(),
+            },
+            MeltPaymentRequest::Bolt12 { offer, invoice: _ } => match offer.amount() {
+                Some(amount) => {
+                    let (amount, currency) = match amount {
+                        lightning::offers::offer::Amount::Bitcoin { amount_msats } => {
+                            (amount_msats, CurrencyUnit::Msat)
+                        }
+                        lightning::offers::offer::Amount::Currency {
+                            iso4217_code,
+                            amount,
+                        } => (
+                            amount,
+                            CurrencyUnit::from_str(&String::from_utf8(iso4217_code.to_vec())?)?,
+                        ),
+                    };
+
+                    to_unit(amount, &currency, &CurrencyUnit::Msat)
+                        .map_err(|_err| Error::UnsupportedUnit)?
+                }
+                None => melt_quote
+                    .options
+                    .ok_or(Error::InvoiceAmountUndefined)?
+                    .amount_msat(),
+            },
         };
 
         let partial_amount = match invoice_amount_msats > quote_msats {
@@ -273,7 +419,7 @@ impl Mint {
                 .map_err(|_| Error::UnsupportedUnit)?,
         };
 
-        let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
+        let inputs_amount_quote_unit = melt_request.inputs_amount().map_err(|_| {
             tracing::error!("Proof inputs in melt quote overflowed");
             Error::AmountOverflow
         })?;
@@ -305,7 +451,7 @@ impl Mint {
         melt_request: &MeltRequest<Uuid>,
     ) -> Result<(ProofWriter, MeltQuote), Error> {
         let (state, quote) = tx
-            .update_melt_quote_state(melt_request.quote(), MeltQuoteState::Pending)
+            .update_melt_quote_state(melt_request.quote(), MeltQuoteState::Pending, None)
             .await?;
 
         match state {
@@ -371,7 +517,7 @@ impl Mint {
 
     /// Melt Bolt11
     #[instrument(skip_all)]
-    pub async fn melt_bolt11(
+    pub async fn melt(
         &self,
         melt_request: &MeltRequest<Uuid>,
     ) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
@@ -455,7 +601,7 @@ impl Mint {
                 tx.commit().await?;
 
                 let pre = match ln
-                    .make_payment(quote.clone(), partial_amount, Some(quote.fee_reserve))
+                    .make_payment(&quote.unit, quote.clone().try_into()?)
                     .await
                 {
                     Ok(pay)
@@ -602,8 +748,12 @@ impl Mint {
             .update_proofs_states(&mut tx, &input_ys, State::Spent)
             .await?;
 
-        tx.update_melt_quote_state(melt_request.quote(), MeltQuoteState::Paid)
-            .await?;
+        tx.update_melt_quote_state(
+            melt_request.quote(),
+            MeltQuoteState::Paid,
+            payment_preimage.clone(),
+        )
+        .await?;
 
         self.pubsub_manager.melt_quote_status(
             &quote,
@@ -615,7 +765,7 @@ impl Mint {
         let mut change = None;
 
         // Check if there is change to return
-        if melt_request.proofs_amount()? > total_spent {
+        if melt_request.inputs_amount()? > total_spent {
             // Check if wallet provided change outputs
             if let Some(outputs) = melt_request.outputs().clone() {
                 let blinded_messages: Vec<PublicKey> =
@@ -636,7 +786,7 @@ impl Mint {
 
                 let fee = self.get_proofs_fee(melt_request.inputs()).await?;
 
-                let change_target = melt_request.proofs_amount()? - total_spent - fee;
+                let change_target = melt_request.inputs_amount()? - total_spent - fee;
 
                 let mut amounts = change_target.split();
                 let mut change_sigs = Vec::with_capacity(amounts.len());
@@ -689,7 +839,7 @@ impl Mint {
             fee_reserve: quote.fee_reserve,
             state: MeltQuoteState::Paid,
             expiry: quote.expiry,
-            request: Some(quote.request.clone()),
+            request: Some(quote.request.to_string()),
             unit: Some(quote.unit.clone()),
         })
     }

+ 42 - 17
crates/cdk/src/mint/mod.rs

@@ -254,11 +254,32 @@ impl Mint {
 
         let mut join_set = JoinSet::new();
 
+        let mut processor_groups: Vec<(
+            Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
+            Vec<PaymentProcessorKey>,
+        )> = Vec::new();
+
         for (key, ln) in self.ln.iter() {
+            // Check if we already have this processor
+            let found = processor_groups.iter_mut().find(|(proc_ref, _)| {
+                // Compare Arc pointer equality using ptr_eq
+                Arc::ptr_eq(proc_ref, ln)
+            });
+
+            if let Some((_, keys)) = found {
+                // We found this processor, add the key to its group
+                keys.push(key.clone());
+            } else {
+                // New processor, create a new group
+                processor_groups.push((Arc::clone(ln), vec![key.clone()]));
+            }
+        }
+
+        for (ln, key) in processor_groups {
             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 ln = Arc::clone(&ln);
                 let shutdown = Arc::clone(&shutdown);
                 let key = key.clone();
                 join_set.spawn(async move {
@@ -274,7 +295,7 @@ impl Mint {
                         match result {
                             Ok(mut stream) => {
                                 while let Some(request_lookup_id) = stream.next().await {
-                                    if let Err(err) = mint.pay_mint_quote_for_request_id(&request_lookup_id).await {
+                                    if let Err(err) = mint.pay_mint_quote_for_request_id(request_lookup_id).await {
                                         tracing::warn!("{:?}", err);
                                     }
                                 }
@@ -416,7 +437,10 @@ impl Mint {
         melt_quote: &MeltQuote,
         melt_request: &MeltRequest<Uuid>,
     ) -> Result<Option<Amount>, Error> {
-        let mint_quote = match tx.get_mint_quote_by_request(&melt_quote.request).await {
+        let mint_quote = match tx
+            .get_mint_quote_by_request(&melt_quote.request.to_string())
+            .await
+        {
             Ok(Some(mint_quote)) => mint_quote,
             // Not an internal melt -> mint
             Ok(None) => return Ok(None),
@@ -428,31 +452,32 @@ impl Mint {
         tracing::error!("internal stuff");
 
         // Mint quote has already been settled, proofs should not be burned or held.
-        if mint_quote.state == MintQuoteState::Issued || mint_quote.state == MintQuoteState::Paid {
+        if mint_quote.state() == MintQuoteState::Issued
+            || mint_quote.state() == MintQuoteState::Paid
+        {
             return Err(Error::RequestAlreadyPaid);
         }
 
-        let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
+        let inputs_amount_quote_unit = melt_request.inputs_amount().map_err(|_| {
             tracing::error!("Proof inputs in melt quote overflowed");
             Error::AmountOverflow
         })?;
 
-        let mut mint_quote = mint_quote;
-
-        if mint_quote.amount > inputs_amount_quote_unit {
-            tracing::debug!(
-                "Not enough inuts provided: {} needed {}",
-                inputs_amount_quote_unit,
-                mint_quote.amount
-            );
-            return Err(Error::InsufficientFunds);
+        if let Some(amount) = mint_quote.amount {
+            if amount > inputs_amount_quote_unit {
+                tracing::debug!(
+                    "Not enough inuts provided: {} needed {}",
+                    inputs_amount_quote_unit,
+                    amount
+                );
+                return Err(Error::InsufficientFunds);
+            }
         }
 
-        mint_quote.state = MintQuoteState::Paid;
-
         let amount = melt_quote.amount;
 
-        tx.add_or_replace_mint_quote(mint_quote).await?;
+        tx.increment_mint_quote_amount_paid(&mint_quote.id, amount, melt_quote.id.to_string())
+            .await?;
 
         Ok(Some(amount))
     }

+ 6 - 2
crates/cdk/src/mint/start_up_check.rs

@@ -10,7 +10,7 @@ use crate::types::PaymentProcessorKey;
 impl Mint {
     /// Checks the states of melt quotes that are **PENDING** or **UNKNOWN** to the mint with the ln node
     pub async fn check_pending_melt_quotes(&self) -> Result<(), Error> {
-        let melt_quotes = self.localstore.get_melt_quotes().await?;
+        let melt_quotes = self.localstore.get_melt_quotes().await.unwrap();
         let pending_quotes: Vec<MeltQuote> = melt_quotes
             .into_iter()
             .filter(|q| q.state == MeltQuoteState::Pending || q.state == MeltQuoteState::Unknown)
@@ -53,7 +53,11 @@ impl Mint {
             };
 
             if let Err(err) = tx
-                .update_melt_quote_state(&pending_quote.id, melt_quote_state)
+                .update_melt_quote_state(
+                    &pending_quote.id,
+                    melt_quote_state,
+                    pay_invoice_response.payment_proof,
+                )
                 .await
             {
                 tracing::error!(

+ 6 - 0
crates/cdk/src/mint/subscription/on_subscription.rs

@@ -48,6 +48,12 @@ impl OnNewSubscription for OnSubscription {
                 Notification::MintQuoteBolt11(uuid) => {
                     mint_queries.push(datastore.get_mint_quote(uuid))
                 }
+                Notification::MintQuoteBolt12(uuid) => {
+                    mint_queries.push(datastore.get_mint_quote(uuid))
+                }
+                Notification::MeltQuoteBolt12(uuid) => {
+                    melt_queries.push(datastore.get_melt_quote(uuid))
+                }
             }
         }
 

+ 23 - 12
crates/cdk/src/wallet/mint.rs → crates/cdk/src/wallet/issue/issue_bolt11.rs

@@ -1,10 +1,10 @@
 use std::collections::HashMap;
 
 use cdk_common::nut04::MintMethodOptions;
-use cdk_common::wallet::{Transaction, TransactionDirection};
+use cdk_common::wallet::{MintQuote, Transaction, TransactionDirection};
+use cdk_common::PaymentMethod;
 use tracing::instrument;
 
-use super::MintQuote;
 use crate::amount::SplitTarget;
 use crate::dhke::construct_proofs;
 use crate::nuts::nut00::ProofsMethods;
@@ -81,16 +81,16 @@ impl Wallet {
 
         let quote_res = self.client.post_mint_quote(request).await?;
 
-        let quote = MintQuote {
+        let quote = MintQuote::new(
+            quote_res.quote,
             mint_url,
-            id: quote_res.quote,
-            amount,
+            PaymentMethod::Bolt11,
+            Some(amount),
             unit,
-            request: quote_res.request,
-            state: quote_res.state,
-            expiry: quote_res.expiry.unwrap_or(0),
-            secret_key: Some(secret_key),
-        };
+            quote_res.request,
+            quote_res.expiry.unwrap_or(0),
+            Some(secret_key),
+        );
 
         self.localstore.add_mint_quote(quote.clone()).await?;
 
@@ -196,6 +196,17 @@ impl Wallet {
             .await?
             .ok_or(Error::UnknownQuote)?;
 
+        if quote_info.payment_method != PaymentMethod::Bolt11 {
+            return Err(Error::UnsupportedPaymentMethod);
+        }
+
+        let amount_mintable = quote_info.amount_mintable();
+
+        if amount_mintable == Amount::ZERO {
+            tracing::debug!("Amount mintable 0.");
+            return Err(Error::AmountUndefined);
+        }
+
         let unix_time = unix_time();
 
         if quote_info.expiry > unix_time {
@@ -214,7 +225,7 @@ impl Wallet {
         let premint_secrets = match &spending_conditions {
             Some(spending_conditions) => PreMintSecrets::with_conditions(
                 active_keyset_id,
-                quote_info.amount,
+                amount_mintable,
                 &amount_split_target,
                 spending_conditions,
             )?,
@@ -222,7 +233,7 @@ impl Wallet {
                 active_keyset_id,
                 count,
                 self.xpriv,
-                quote_info.amount,
+                amount_mintable,
                 &amount_split_target,
             )?,
         };

+ 258 - 0
crates/cdk/src/wallet/issue/issue_bolt12.rs

@@ -0,0 +1,258 @@
+use std::collections::HashMap;
+
+use cdk_common::nut04::MintMethodOptions;
+use cdk_common::nut24::MintQuoteBolt12Request;
+use cdk_common::wallet::{Transaction, TransactionDirection};
+use cdk_common::{Proofs, SecretKey};
+use tracing::instrument;
+
+use crate::amount::SplitTarget;
+use crate::dhke::construct_proofs;
+use crate::nuts::nut00::ProofsMethods;
+use crate::nuts::{
+    nut12, MintQuoteBolt12Response, MintRequest, PaymentMethod, PreMintSecrets, SpendingConditions,
+    State,
+};
+use crate::types::ProofInfo;
+use crate::util::unix_time;
+use crate::wallet::MintQuote;
+use crate::{Amount, Error, Wallet};
+
+impl Wallet {
+    /// Mint Bolt12
+    #[instrument(skip(self))]
+    pub async fn mint_bolt12_quote(
+        &self,
+        amount: Option<Amount>,
+        description: Option<String>,
+    ) -> Result<MintQuote, Error> {
+        let mint_url = self.mint_url.clone();
+        let unit = &self.unit;
+
+        // If we have a description, we check that the mint supports it.
+        if description.is_some() {
+            let mint_method_settings = self
+                .localstore
+                .get_mint(mint_url.clone())
+                .await?
+                .ok_or(Error::IncorrectMint)?
+                .nuts
+                .nut04
+                .get_settings(unit, &crate::nuts::PaymentMethod::Bolt12)
+                .ok_or(Error::UnsupportedUnit)?;
+
+            match mint_method_settings.options {
+                Some(MintMethodOptions::Bolt11 { description }) if description => (),
+                _ => return Err(Error::InvoiceDescriptionUnsupported),
+            }
+        }
+
+        let secret_key = SecretKey::generate();
+
+        let mint_request = MintQuoteBolt12Request {
+            amount,
+            unit: self.unit.clone(),
+            description,
+            pubkey: secret_key.public_key(),
+        };
+
+        let quote_res = self.client.post_mint_bolt12_quote(mint_request).await?;
+
+        let quote = MintQuote::new(
+            quote_res.quote,
+            mint_url,
+            PaymentMethod::Bolt12,
+            amount,
+            unit.clone(),
+            quote_res.request,
+            quote_res.expiry.unwrap_or(0),
+            Some(secret_key),
+        );
+
+        self.localstore.add_mint_quote(quote.clone()).await?;
+
+        Ok(quote)
+    }
+
+    /// Mint bolt12
+    #[instrument(skip(self))]
+    pub async fn mint_bolt12(
+        &self,
+        quote_id: &str,
+        amount: Option<Amount>,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+    ) -> Result<Proofs, Error> {
+        // Check that mint is in store of mints
+        if self
+            .localstore
+            .get_mint(self.mint_url.clone())
+            .await?
+            .is_none()
+        {
+            self.get_mint_info().await?;
+        }
+
+        let quote_info = self.localstore.get_mint_quote(quote_id).await?;
+
+        let quote_info = if let Some(quote) = quote_info {
+            if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) {
+                return Err(Error::ExpiredQuote(quote.expiry, unix_time()));
+            }
+
+            quote.clone()
+        } else {
+            return Err(Error::UnknownQuote);
+        };
+
+        let active_keyset_id = self.get_active_mint_keyset().await?.id;
+
+        let count = self
+            .localstore
+            .get_keyset_counter(&active_keyset_id)
+            .await?;
+
+        let count = count.map_or(0, |c| c + 1);
+
+        let amount = match amount {
+            Some(amount) => amount,
+            None => {
+                // If an amount it not supplied with check the status of the quote
+                // The mint will tell us how much can be minted
+                let state = self.mint_bolt12_quote_state(quote_id).await?;
+
+                state.amount_paid - state.amount_issued
+            }
+        };
+
+        if amount == Amount::ZERO {
+            tracing::error!("Cannot mint zero amount.");
+            return Err(Error::InvoiceAmountUndefined);
+        }
+
+        let premint_secrets = match &spending_conditions {
+            Some(spending_conditions) => PreMintSecrets::with_conditions(
+                active_keyset_id,
+                amount,
+                &amount_split_target,
+                spending_conditions,
+            )?,
+            None => PreMintSecrets::from_xpriv(
+                active_keyset_id,
+                count,
+                self.xpriv,
+                amount,
+                &amount_split_target,
+            )?,
+        };
+
+        let mut request = MintRequest {
+            quote: quote_id.to_string(),
+            outputs: premint_secrets.blinded_messages(),
+            signature: None,
+        };
+
+        if let Some(secret_key) = quote_info.secret_key.clone() {
+            request.sign(secret_key)?;
+        } else {
+            tracing::error!("Signature is required for bolt12.");
+            return Err(Error::SignatureMissingOrInvalid);
+        }
+
+        let mint_res = self.client.post_mint(request).await?;
+
+        let keys = self.get_keyset_keys(active_keyset_id).await?;
+
+        // Verify the signature DLEQ is valid
+        {
+            for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
+                let keys = self.get_keyset_keys(sig.keyset_id).await?;
+                let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
+                match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
+                    Ok(_) | Err(nut12::Error::MissingDleqProof) => (),
+                    Err(_) => return Err(Error::CouldNotVerifyDleq),
+                }
+            }
+        }
+
+        let proofs = construct_proofs(
+            mint_res.signatures,
+            premint_secrets.rs(),
+            premint_secrets.secrets(),
+            &keys,
+        )?;
+
+        // Remove filled quote from store
+        let mut quote_info = self
+            .localstore
+            .get_mint_quote(quote_id)
+            .await?
+            .ok_or(Error::UnpaidQuote)?;
+        quote_info.amount_issued += proofs.total_amount()?;
+
+        self.localstore.add_mint_quote(quote_info.clone()).await?;
+
+        if spending_conditions.is_none() {
+            // Update counter for keyset
+            self.localstore
+                .increment_keyset_counter(&active_keyset_id, proofs.len() as u32)
+                .await?;
+        }
+
+        let proof_infos = proofs
+            .iter()
+            .map(|proof| {
+                ProofInfo::new(
+                    proof.clone(),
+                    self.mint_url.clone(),
+                    State::Unspent,
+                    quote_info.unit.clone(),
+                )
+            })
+            .collect::<Result<Vec<ProofInfo>, _>>()?;
+
+        // Add new proofs to store
+        self.localstore.update_proofs(proof_infos, vec![]).await?;
+
+        // Add transaction to store
+        self.localstore
+            .add_transaction(Transaction {
+                mint_url: self.mint_url.clone(),
+                direction: TransactionDirection::Incoming,
+                amount: proofs.total_amount()?,
+                fee: Amount::ZERO,
+                unit: self.unit.clone(),
+                ys: proofs.ys()?,
+                timestamp: unix_time(),
+                memo: None,
+                metadata: HashMap::new(),
+            })
+            .await?;
+
+        Ok(proofs)
+    }
+
+    /// Check mint quote status
+    #[instrument(skip(self, quote_id))]
+    pub async fn mint_bolt12_quote_state(
+        &self,
+        quote_id: &str,
+    ) -> Result<MintQuoteBolt12Response<String>, Error> {
+        let response = self.client.get_mint_quote_bolt12_status(quote_id).await?;
+
+        match self.localstore.get_mint_quote(quote_id).await? {
+            Some(quote) => {
+                let mut quote = quote;
+                quote.amount_issued = response.amount_issued;
+                quote.amount_paid = response.amount_paid;
+
+                self.localstore.add_mint_quote(quote).await?;
+            }
+            None => {
+                tracing::info!("Quote mint {} unknown", quote_id);
+            }
+        }
+
+        Ok(response)
+    }
+}

+ 2 - 0
crates/cdk/src/wallet/issue/mod.rs

@@ -0,0 +1,2 @@
+mod issue_bolt11;
+mod issue_bolt12;

+ 1 - 1
crates/cdk/src/wallet/melt.rs → crates/cdk/src/wallet/melt/melt_bolt11.rs

@@ -6,7 +6,6 @@ use cdk_common::wallet::{Transaction, TransactionDirection};
 use lightning_invoice::Bolt11Invoice;
 use tracing::instrument;
 
-use super::MeltQuote;
 use crate::amount::to_unit;
 use crate::dhke::construct_proofs;
 use crate::nuts::{
@@ -15,6 +14,7 @@ use crate::nuts::{
 };
 use crate::types::{Melted, ProofInfo};
 use crate::util::unix_time;
+use crate::wallet::MeltQuote;
 use crate::{ensure_cdk, Error, Wallet};
 
 impl Wallet {

+ 89 - 0
crates/cdk/src/wallet/melt/melt_bolt12.rs

@@ -0,0 +1,89 @@
+//! Melt BOLT12
+//!
+//! Implementation of melt functionality for BOLT12 offers
+
+use std::str::FromStr;
+
+use cdk_common::amount::amount_for_offer;
+use cdk_common::wallet::MeltQuote;
+use lightning::offers::offer::Offer;
+use tracing::instrument;
+
+use crate::amount::to_unit;
+use crate::nuts::{CurrencyUnit, MeltOptions, MeltQuoteBolt11Response, MeltQuoteBolt12Request};
+use crate::{Error, Wallet};
+
+impl Wallet {
+    /// Melt Quote for BOLT12 offer
+    #[instrument(skip(self, request))]
+    pub async fn melt_bolt12_quote(
+        &self,
+        request: String,
+        options: Option<MeltOptions>,
+    ) -> Result<MeltQuote, Error> {
+        let quote_request = MeltQuoteBolt12Request {
+            request: request.clone(),
+            unit: self.unit.clone(),
+            options,
+        };
+
+        let quote_res = self.client.post_melt_bolt12_quote(quote_request).await?;
+
+        if self.unit == CurrencyUnit::Sat || self.unit == CurrencyUnit::Msat {
+            let offer = Offer::from_str(&request).map_err(|_| Error::Bolt12parse)?;
+            // Get amount from offer or options
+            let amount_msat = options
+                .map(|opt| opt.amount_msat())
+                .or_else(|| amount_for_offer(&offer, &CurrencyUnit::Msat).ok())
+                .ok_or(Error::AmountUndefined)?;
+            let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit).unwrap();
+
+            if quote_res.amount != amount_quote_unit {
+                tracing::warn!(
+                    "Mint returned incorrect quote amount. Expected {}, got {}",
+                    amount_quote_unit,
+                    quote_res.amount
+                );
+                return Err(Error::IncorrectQuoteAmount);
+            }
+        }
+
+        let quote = MeltQuote {
+            id: quote_res.quote,
+            amount: quote_res.amount,
+            request,
+            unit: self.unit.clone(),
+            fee_reserve: quote_res.fee_reserve,
+            state: quote_res.state,
+            expiry: quote_res.expiry,
+            payment_preimage: quote_res.payment_preimage,
+        };
+
+        self.localstore.add_melt_quote(quote.clone()).await?;
+
+        Ok(quote)
+    }
+
+    /// BOLT12 melt quote status
+    #[instrument(skip(self, quote_id))]
+    pub async fn melt_bolt12_quote_status(
+        &self,
+        quote_id: &str,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        let response = self.client.get_melt_bolt12_quote_status(quote_id).await?;
+
+        match self.localstore.get_melt_quote(quote_id).await? {
+            Some(quote) => {
+                let mut quote = quote;
+
+                quote.state = response.state;
+                self.localstore.add_melt_quote(quote).await?;
+            }
+            None => {
+                tracing::info!("Quote melt {} unknown", quote_id);
+            }
+        }
+
+        Ok(response)
+    }
+}

+ 2 - 0
crates/cdk/src/wallet/melt/mod.rs

@@ -0,0 +1,2 @@
+mod melt_bolt11;
+mod melt_bolt12;

+ 101 - 1
crates/cdk/src/wallet/mint_connector/http_client.rs

@@ -2,6 +2,7 @@
 use std::sync::Arc;
 
 use async_trait::async_trait;
+use cdk_common::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
 #[cfg(feature = "auth")]
 use cdk_common::{Method, ProtectedEndpoint, RoutePath};
 use reqwest::{Client, IntoUrl};
@@ -91,7 +92,9 @@ impl HttpClientCore {
         let response = request
             .send()
             .await
-            .map_err(|e| Error::HttpError(e.to_string()))?
+            .map_err(|e| Error::HttpError(e.to_string()))?;
+
+        let response = response
             .text()
             .await
             .map_err(|e| Error::HttpError(e.to_string()))?;
@@ -395,6 +398,103 @@ impl MintConnector for HttpClient {
         let auth_token = None;
         self.core.http_post(url, auth_token, &request).await
     }
+
+    /// Mint Quote Bolt12 [NUT-23]
+    #[instrument(skip(self), fields(mint_url = %self.mint_url))]
+    async fn post_mint_bolt12_quote(
+        &self,
+        request: MintQuoteBolt12Request,
+    ) -> Result<MintQuoteBolt12Response<String>, Error> {
+        let url = self
+            .mint_url
+            .join_paths(&["v1", "mint", "quote", "bolt12"])?;
+
+        #[cfg(feature = "auth")]
+        let auth_token = self
+            .get_auth_token(Method::Post, RoutePath::MintQuoteBolt12)
+            .await?;
+
+        #[cfg(not(feature = "auth"))]
+        let auth_token = None;
+
+        self.core.http_post(url, auth_token, &request).await
+    }
+
+    /// Mint Quote Bolt12 status
+    #[instrument(skip(self), fields(mint_url = %self.mint_url))]
+    async fn get_mint_quote_bolt12_status(
+        &self,
+        quote_id: &str,
+    ) -> Result<MintQuoteBolt12Response<String>, Error> {
+        let url = self
+            .mint_url
+            .join_paths(&["v1", "mint", "quote", "bolt12", quote_id])?;
+
+        #[cfg(feature = "auth")]
+        let auth_token = self
+            .get_auth_token(Method::Get, RoutePath::MintQuoteBolt12)
+            .await?;
+
+        #[cfg(not(feature = "auth"))]
+        let auth_token = None;
+        self.core.http_get(url, auth_token).await
+    }
+
+    /// Melt Quote Bolt12 [NUT-23]
+    #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
+    async fn post_melt_bolt12_quote(
+        &self,
+        request: MeltQuoteBolt12Request,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        let url = self
+            .mint_url
+            .join_paths(&["v1", "melt", "quote", "bolt12"])?;
+        #[cfg(feature = "auth")]
+        let auth_token = self
+            .get_auth_token(Method::Post, RoutePath::MeltQuoteBolt12)
+            .await?;
+
+        #[cfg(not(feature = "auth"))]
+        let auth_token = None;
+        self.core.http_post(url, auth_token, &request).await
+    }
+
+    /// Melt Quote Bolt12 Status [NUT-23]
+    #[instrument(skip(self), fields(mint_url = %self.mint_url))]
+    async fn get_melt_bolt12_quote_status(
+        &self,
+        quote_id: &str,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        let url = self
+            .mint_url
+            .join_paths(&["v1", "melt", "quote", "bolt12", quote_id])?;
+
+        #[cfg(feature = "auth")]
+        let auth_token = self
+            .get_auth_token(Method::Get, RoutePath::MeltQuoteBolt12)
+            .await?;
+
+        #[cfg(not(feature = "auth"))]
+        let auth_token = None;
+        self.core.http_get(url, auth_token).await
+    }
+
+    /// Melt Bolt12 [NUT-23]
+    #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
+    async fn post_melt_bolt12(
+        &self,
+        request: MeltRequest<String>,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        let url = self.mint_url.join_paths(&["v1", "melt", "bolt12"])?;
+        #[cfg(feature = "auth")]
+        let auth_token = self
+            .get_auth_token(Method::Post, RoutePath::MeltBolt12)
+            .await?;
+
+        #[cfg(not(feature = "auth"))]
+        let auth_token = None;
+        self.core.http_post(url, auth_token, &request).await
+    }
 }
 
 /// Http Client

+ 26 - 0
crates/cdk/src/wallet/mint_connector/mod.rs

@@ -3,6 +3,7 @@
 use std::fmt::Debug;
 
 use async_trait::async_trait;
+use cdk_common::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
 
 use super::Error;
 use crate::nuts::{
@@ -77,4 +78,29 @@ pub trait MintConnector: Debug {
     /// Set auth wallet on client
     #[cfg(feature = "auth")]
     async fn set_auth_wallet(&self, wallet: Option<AuthWallet>);
+    /// Mint Quote [NUT-04]
+    async fn post_mint_bolt12_quote(
+        &self,
+        request: MintQuoteBolt12Request,
+    ) -> Result<MintQuoteBolt12Response<String>, Error>;
+    /// Mint Quote status
+    async fn get_mint_quote_bolt12_status(
+        &self,
+        quote_id: &str,
+    ) -> Result<MintQuoteBolt12Response<String>, Error>;
+    /// Melt Quote [NUT-23]
+    async fn post_melt_bolt12_quote(
+        &self,
+        request: MeltQuoteBolt12Request,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error>;
+    /// Melt Quote Status [NUT-23]
+    async fn get_melt_bolt12_quote_status(
+        &self,
+        quote_id: &str,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error>;
+    /// Melt [NUT-23]
+    async fn post_melt_bolt12(
+        &self,
+        request: MeltRequest<String>,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error>;
 }

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

@@ -34,9 +34,9 @@ use crate::OidcClient;
 mod auth;
 mod balance;
 mod builder;
+mod issue;
 mod keysets;
 mod melt;
-mod mint;
 mod mint_connector;
 pub mod multi_mint_wallet;
 mod proofs;

+ 7 - 0
misc/itests.sh

@@ -224,6 +224,13 @@ if [ $? -ne 0 ]; then
     exit 1
 fi
 
+echo "Running regtest test with cln mint for bolt12"
+cargo test -p cdk-integration-tests --test bolt12
+if [ $? -ne 0 ]; then
+    echo "regtest test failed, exiting"
+    exit 1
+fi
+
 # Switch Mints: Run tests with LND mint
 echo "Switching to LND mint for tests"
 export CDK_ITESTS_MINT_PORT_0=8087

+ 14 - 0
misc/mintd_payment_processor.sh

@@ -50,6 +50,7 @@ cleanup() {
     unset CDK_MINTD_GRPC_PAYMENT_PROCESSOR_SUPPORTED_UNITS
     unset CDK_MINTD_MNEMONIC
     unset CDK_MINTD_PID
+    unset CDK_PAYMENT_PROCESSOR_CLN_BOLT12
 }
 
 # Set up trap to call cleanup on script exit
@@ -102,6 +103,7 @@ if [ "$LN_BACKEND" != "FAKEWALLET" ]; then
         sleep 1
     done
     echo "Regtest set up continuing"
+    export CDK_PAYMENT_PROCESSOR_CLN_BOLT12=true
 fi
 
 # Start payment processor
@@ -177,5 +179,17 @@ cargo test -p cdk-integration-tests --test happy_path_mint_wallet
 # Capture the exit status of cargo test
 test_status=$?
 
+if [ "$LN_BACKEND" = "CLN" ]; then
+    echo "Running bolt12 tests for CLN backend"
+    cargo test -p cdk-integration-tests --test bolt12
+    bolt12_test_status=$?
+    
+    # Exit with non-zero status if either test failed
+    if [ $test_status -ne 0 ] || [ $bolt12_test_status -ne 0 ]; then
+        echo "Tests failed - happy_path_mint_wallet: $test_status, bolt12: $bolt12_test_status"
+        exit 1
+    fi
+fi
+
 # Exit with the status of the tests
 exit $test_status

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません