Browse Source

Merge branch 'main' into main

C 2 months ago
parent
commit
8cf3b2d992
58 changed files with 1576 additions and 823 deletions
  1. 15 0
      CHANGELOG.md
  2. 2 0
      README.md
  3. 3 104
      crates/cashu/src/lib.rs
  4. 2 2
      crates/cashu/src/nuts/mod.rs
  5. 27 19
      crates/cashu/src/nuts/nut13.rs
  6. 1 1
      crates/cashu/src/nuts/nut18/payment_request.rs
  7. 0 0
      crates/cashu/src/nuts/nut25.rs
  8. 100 0
      crates/cashu/src/quote_id.rs
  9. 5 4
      crates/cdk-cln/src/lib.rs
  10. 12 6
      crates/cdk-common/src/database/mint/mod.rs
  11. 4 2
      crates/cdk-common/src/database/mint/test.rs
  12. 9 2
      crates/cdk-common/src/payment.rs
  13. 3 0
      crates/cdk-common/src/wallet.rs
  14. 11 8
      crates/cdk-fake-wallet/src/lib.rs
  15. 18 14
      crates/cdk-integration-tests/tests/bolt12.rs
  16. 1 1
      crates/cdk-integration-tests/tests/fake_auth.rs
  17. 5 5
      crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs
  18. 2 2
      crates/cdk-integration-tests/tests/ldk_node.rs
  19. 4 4
      crates/cdk-integration-tests/tests/regtest.rs
  20. 2 2
      crates/cdk-integration-tests/tests/test_fees.rs
  21. 1 4
      crates/cdk-ldk-node/Cargo.toml
  22. 4 4
      crates/cdk-ldk-node/src/lib.rs
  23. 61 29
      crates/cdk-ldk-node/src/web/handlers/channels.rs
  24. 103 54
      crates/cdk-ldk-node/src/web/handlers/lightning.rs
  25. 54 21
      crates/cdk-ldk-node/src/web/handlers/onchain.rs
  26. 447 144
      crates/cdk-ldk-node/src/web/templates/layout.rs
  27. 1 1
      crates/cdk-ldk-node/src/web/templates/payments.rs
  28. BIN
      crates/cdk-ldk-node/static/images/bg-dark.jpg
  29. 4 4
      crates/cdk-lnbits/src/lib.rs
  30. 5 4
      crates/cdk-lnd/src/lib.rs
  31. 5 3
      crates/cdk-payment-processor/src/proto/client.rs
  32. 15 11
      crates/cdk-payment-processor/src/proto/server.rs
  33. 1 1
      crates/cdk-postgres/Cargo.toml
  34. 44 3
      crates/cdk-postgres/src/lib.rs
  35. 0 1
      crates/cdk-sql-common/Cargo.toml
  36. 29 123
      crates/cdk-sql-common/src/mint/mod.rs
  37. 2 0
      crates/cdk-sql-common/src/wallet/migrations.rs
  38. 1 0
      crates/cdk-sql-common/src/wallet/migrations/postgres/20250831215438_melt_quote_method.sql
  39. 1 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20250831215438_melt_quote_method.sql
  40. 18 6
      crates/cdk-sql-common/src/wallet/mod.rs
  41. 6 0
      crates/cdk/Cargo.toml
  42. 161 0
      crates/cdk/examples/mint-token-bolt12-with-custom-http.rs
  43. 12 8
      crates/cdk/src/mint/mod.rs
  44. 39 26
      crates/cdk/src/pub_sub.rs
  45. 16 1
      crates/cdk/src/wallet/builder.rs
  46. 1 1
      crates/cdk/src/wallet/issue/issue_bolt12.rs
  47. 9 1
      crates/cdk/src/wallet/melt/melt_bolt11.rs
  48. 2 0
      crates/cdk/src/wallet/melt/melt_bolt12.rs
  49. 60 156
      crates/cdk/src/wallet/mint_connector/http_client.rs
  50. 6 3
      crates/cdk/src/wallet/mint_connector/mod.rs
  51. 182 0
      crates/cdk/src/wallet/mint_connector/transport.rs
  52. 4 0
      crates/cdk/src/wallet/mod.rs
  53. 21 1
      crates/cdk/src/wallet/subscription/http.rs
  54. 9 1
      crates/cdk/src/wallet/subscription/mod.rs
  55. 10 26
      crates/cdk/src/wallet/subscription/ws.rs
  56. 9 9
      flake.lock
  57. 5 1
      flake.nix
  58. 2 0
      justfile

+ 15 - 0
CHANGELOG.md

@@ -6,6 +6,21 @@
 
 
 ## [Unreleased]
 ## [Unreleased]
 
 
+### Added
+- cdk-common: New `Event` enum for payment event handling with `PaymentReceived` variant ([thesimplekid]).
+- cdk-common: Added `payment_method` field to `MeltQuote` struct for tracking payment method type ([thesimplekid]).
+- cdk-sql-common: Database migration to add `payment_method` column to melt_quote table for SQLite and PostgreSQL ([thesimplekid]).
+
+### Changed
+- cdk-common: Refactored `MintPayment` trait method `wait_any_incoming_payment` to `wait_payment_event` with event-driven architecture ([thesimplekid]).
+- cdk-common: Updated `wait_payment_event` return type to stream `Event` enum instead of `WaitPaymentResponse` directly ([thesimplekid]).
+- cdk: Updated mint payment handling to process payment events through new `Event` enum pattern ([thesimplekid]).
+- cashu: Updated BOLT12 payment method specification from NUT-24 to NUT-25 ([thesimplekid]).
+- cdk: Updated BOLT12 import references from nut24 to nut25 module ([thesimplekid]).
+
+### Fixied
+- cdk: Wallet melt track and use payment method from quote for BOLT11/BOLT12 routing ([thesimplekid]).
+
 ## [0.12.0](https://github.com/cashubtc/cdk/releases/tag/v0.12.0)
 ## [0.12.0](https://github.com/cashubtc/cdk/releases/tag/v0.12.0)
 
 
 ### Summary
 ### Summary

+ 2 - 0
README.md

@@ -87,6 +87,7 @@ gossip_source_type = "rgs"
 | [21][21] | Clear Authentication | :heavy_check_mark: |
 | [21][21] | Clear Authentication | :heavy_check_mark: |
 | [22][22] | Blind Authentication  | :heavy_check_mark: |
 | [22][22] | Blind Authentication  | :heavy_check_mark: |
 | [23][23] | Payment Method: BOLT11 | :heavy_check_mark: |
 | [23][23] | Payment Method: BOLT11 | :heavy_check_mark: |
+| [25][25] | Payment Method: BOLT12 | :heavy_check_mark: |
 
 
 
 
 ## License
 ## License
@@ -126,3 +127,4 @@ Please see the [development guide](DEVELOPMENT.md).
 [21]: https://github.com/cashubtc/nuts/blob/main/21.md
 [21]: https://github.com/cashubtc/nuts/blob/main/21.md
 [22]: https://github.com/cashubtc/nuts/blob/main/22.md
 [22]: https://github.com/cashubtc/nuts/blob/main/22.md
 [23]: https://github.com/cashubtc/nuts/blob/main/23.md
 [23]: https://github.com/cashubtc/nuts/blob/main/23.md
+[25]: https://github.com/cashubtc/nuts/blob/main/25.md

+ 3 - 104
crates/cashu/src/lib.rs

@@ -16,6 +16,9 @@ pub use self::mint_url::MintUrl;
 pub use self::nuts::*;
 pub use self::nuts::*;
 pub use self::util::SECP256K1;
 pub use self::util::SECP256K1;
 
 
+#[cfg(feature = "mint")]
+pub mod quote_id;
+
 #[doc(hidden)]
 #[doc(hidden)]
 #[macro_export]
 #[macro_export]
 macro_rules! ensure_cdk {
 macro_rules! ensure_cdk {
@@ -25,107 +28,3 @@ macro_rules! ensure_cdk {
         }
         }
     };
     };
 }
 }
-
-#[cfg(feature = "mint")]
-/// Quote ID. The specifications only define a string but CDK uses Uuid, so we use an enum to port compatibility.
-pub mod quote_id {
-    use std::fmt;
-    use std::str::FromStr;
-
-    use bitcoin::base64::engine::general_purpose;
-    use bitcoin::base64::Engine as _;
-    use serde::{de, Deserialize, Deserializer, Serialize};
-    use thiserror::Error;
-    use uuid::Uuid;
-
-    /// Invalid UUID
-    #[derive(Debug, Error)]
-    pub enum QuoteIdError {
-        /// UUID Error
-        #[error("invalid UUID: {0}")]
-        Uuid(#[from] uuid::Error),
-        /// Invalid base64
-        #[error("invalid base64")]
-        Base64,
-        /// Invalid quote ID
-        #[error("neither a valid UUID nor a valid base64 string")]
-        InvalidQuoteId,
-    }
-
-    /// Mint Quote ID
-    #[derive(Serialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
-    #[serde(untagged)]
-    pub enum QuoteId {
-        /// (Nutshell) base64 quote ID
-        BASE64(String),
-        /// UUID quote ID
-        UUID(Uuid),
-    }
-
-    impl QuoteId {
-        /// Create a new UUID-based MintQuoteId
-        pub fn new_uuid() -> Self {
-            Self::UUID(Uuid::new_v4())
-        }
-    }
-
-    impl From<Uuid> for QuoteId {
-        fn from(uuid: Uuid) -> Self {
-            Self::UUID(uuid)
-        }
-    }
-
-    impl fmt::Display for QuoteId {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            match self {
-                QuoteId::BASE64(s) => write!(f, "{}", s),
-                QuoteId::UUID(u) => write!(f, "{}", u),
-            }
-        }
-    }
-
-    impl FromStr for QuoteId {
-        type Err = QuoteIdError;
-
-        fn from_str(s: &str) -> Result<Self, Self::Err> {
-            // Try UUID first
-            if let Ok(u) = Uuid::parse_str(s) {
-                return Ok(QuoteId::UUID(u));
-            }
-
-            // Try base64: decode, then re-encode and compare to ensure canonical form
-            // Use the standard (URL/filename safe or standard) depending on your needed alphabet.
-            // Here we use standard base64.
-            match general_purpose::URL_SAFE.decode(s) {
-                Ok(_bytes) => Ok(QuoteId::BASE64(s.to_string())),
-                Err(_) => Err(QuoteIdError::InvalidQuoteId),
-            }
-        }
-    }
-
-    impl<'de> Deserialize<'de> for QuoteId {
-        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-        where
-            D: Deserializer<'de>,
-        {
-            // Deserialize as plain string first
-            let s = String::deserialize(deserializer)?;
-
-            // Try UUID first
-            if let Ok(u) = Uuid::parse_str(&s) {
-                return Ok(QuoteId::UUID(u));
-            }
-
-            if general_purpose::URL_SAFE.decode(&s).is_ok() {
-                return Ok(QuoteId::BASE64(s));
-            }
-
-            // Neither matched — return a helpful error
-            Err(de::Error::custom(format!(
-                "QuoteId must be either a UUID (e.g. {}) or a valid base64 string; got: {}",
-                Uuid::nil(),
-                s
-            )))
-        }
-    }
-}

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

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

+ 27 - 19
crates/cashu/src/nuts/nut13.rs

@@ -3,7 +3,7 @@
 //! <https://github.com/cashubtc/nuts/blob/main/13.md>
 //! <https://github.com/cashubtc/nuts/blob/main/13.md>
 
 
 use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
 use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
-use bitcoin::secp256k1::hashes::{hmac, sha512, Hash, HashEngine, HmacEngine};
+use bitcoin::secp256k1::hashes::{hmac, sha256, Hash, HashEngine, HmacEngine};
 use bitcoin::{secp256k1, Network};
 use bitcoin::{secp256k1, Network};
 use thiserror::Error;
 use thiserror::Error;
 use tracing::instrument;
 use tracing::instrument;
@@ -66,14 +66,14 @@ impl Secret {
 
 
     fn derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
     fn derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
         let mut message = Vec::new();
         let mut message = Vec::new();
-        message.extend_from_slice(b"Cashu_KDF_HMAC_SHA512");
+        message.extend_from_slice(b"Cashu_KDF_HMAC_SHA256");
         message.extend_from_slice(&keyset_id.to_bytes());
         message.extend_from_slice(&keyset_id.to_bytes());
         message.extend_from_slice(&(counter as u64).to_be_bytes());
         message.extend_from_slice(&(counter as u64).to_be_bytes());
         message.extend_from_slice(b"\x00");
         message.extend_from_slice(b"\x00");
 
 
-        let mut engine = HmacEngine::<sha512::Hash>::new(seed);
+        let mut engine = HmacEngine::<sha256::Hash>::new(seed);
         engine.input(&message);
         engine.input(&message);
-        let hmac_result = hmac::Hmac::<sha512::Hash>::from_engine(engine);
+        let hmac_result = hmac::Hmac::<sha256::Hash>::from_engine(engine);
         let result_bytes = hmac_result.to_byte_array();
         let result_bytes = hmac_result.to_byte_array();
 
 
         Ok(Self::new(hex::encode(&result_bytes[..32])))
         Ok(Self::new(hex::encode(&result_bytes[..32])))
@@ -101,14 +101,14 @@ impl SecretKey {
 
 
     fn derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
     fn derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
         let mut message = Vec::new();
         let mut message = Vec::new();
-        message.extend_from_slice(b"Cashu_KDF_HMAC_SHA512");
+        message.extend_from_slice(b"Cashu_KDF_HMAC_SHA256");
         message.extend_from_slice(&keyset_id.to_bytes());
         message.extend_from_slice(&keyset_id.to_bytes());
         message.extend_from_slice(&(counter as u64).to_be_bytes());
         message.extend_from_slice(&(counter as u64).to_be_bytes());
         message.extend_from_slice(b"\x01");
         message.extend_from_slice(b"\x01");
 
 
-        let mut engine = HmacEngine::<sha512::Hash>::new(seed);
+        let mut engine = HmacEngine::<sha256::Hash>::new(seed);
         engine.input(&message);
         engine.input(&message);
-        let hmac_result = hmac::Hmac::<sha512::Hash>::from_engine(engine);
+        let hmac_result = hmac::Hmac::<sha256::Hash>::from_engine(engine);
         let result_bytes = hmac_result.to_byte_array();
         let result_bytes = hmac_result.to_byte_array();
 
 
         Ok(Self::from(secp256k1::SecretKey::from_slice(
         Ok(Self::from(secp256k1::SecretKey::from_slice(
@@ -316,26 +316,26 @@ mod tests {
 
 
         // Test with a v2 keyset ID (33 bytes, starting with "01")
         // Test with a v2 keyset ID (33 bytes, starting with "01")
         let keyset_id =
         let keyset_id =
-            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
+            Id::from_str("012e23479a0029432eaad0d2040c09be53bab592d5cbf1d55e0dd26c9495951b30")
                 .unwrap();
                 .unwrap();
 
 
         // Expected secrets derived using the new derivation
         // Expected secrets derived using the new derivation
         let test_secrets = [
         let test_secrets = [
-            "f24ca2e4e5c8e1e8b43e3d0d9e9d4c2a1b6a5e9f8c7b3d2e1f0a9b8c7d6e5f4a",
-            "8b7e5f9a4d3c2b1e7f6a5d9c8b4e3f2a6b5c9d8e7f4a3b2e1f5a9c8d7b6e4f3",
-            "e9f8c7b6a5d4c3b2a1f9e8d7c6b5a4d3c2b1f0e9d8c7b6a5f4e3d2c1b0a9f8e7",
-            "a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2",
-            "d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6",
+            "ba250bf927b1df5dd0a07c543be783a4349a7f99904acd3406548402d3484118",
+            "3a6423fe56abd5e74ec9d22a91ee110cd2ce45a7039901439d62e5534d3438c1",
+            "843484a75b78850096fac5b513e62854f11d57491cf775a6fd2edf4e583ae8c0",
+            "3600608d5cf8197374f060cfbcff134d2cd1fb57eea68cbcf2fa6917c58911b6",
+            "717fce9cc6f9ea060d20dd4e0230af4d63f3894cc49dd062fd99d033ea1ac1dd",
         ];
         ];
 
 
-        for (i, _test_secret) in test_secrets.iter().enumerate() {
+        for (i, test_secret) in test_secrets.iter().enumerate() {
             let secret = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
             let secret = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
             // Note: The actual expected values would need to be computed from a reference implementation
             // Note: The actual expected values would need to be computed from a reference implementation
             // For now, we just verify the derivation works and produces consistent results
             // For now, we just verify the derivation works and produces consistent results
             assert_eq!(secret.to_string().len(), 64); // Should be 32 bytes = 64 hex chars
             assert_eq!(secret.to_string().len(), 64); // Should be 32 bytes = 64 hex chars
 
 
             // Test deterministic derivation: same inputs should produce same outputs
             // Test deterministic derivation: same inputs should produce same outputs
-            let secret2 = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
+            let secret2 = Secret::from_str(test_secret).unwrap();
             assert_eq!(secret, secret2);
             assert_eq!(secret, secret2);
         }
         }
     }
     }
@@ -349,18 +349,26 @@ mod tests {
 
 
         // Test with a v2 keyset ID (33 bytes, starting with "01")
         // Test with a v2 keyset ID (33 bytes, starting with "01")
         let keyset_id =
         let keyset_id =
-            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
+            Id::from_str("012e23479a0029432eaad0d2040c09be53bab592d5cbf1d55e0dd26c9495951b30")
                 .unwrap();
                 .unwrap();
 
 
-        for i in 0..5 {
-            let secret_key = SecretKey::from_seed(&seed, keyset_id, i).unwrap();
+        let test_secret_keys = [
+            "4f8b32a54aed811b692a665ed296b4c1fc2f37a8be4006379e95063a76693745",
+            "c4b8412ee644067007423480c9e556385b71ffdff0f340bc16a95c0534fe0e01",
+            "ceff40983441c40acaf77d2a8ddffd5c1c84391fb9fd0dc4607c186daab1c829",
+            "41ad26b840fb62d29b2318a82f1d9cd40dc0f1e58183cc57562f360a32fdfad6",
+            "fb986a9c76758593b0e2d1a5172ade977c858d87111a220e16c292a9347abf81",
+        ];
+
+        for (i, test_secret) in test_secret_keys.iter().enumerate() {
+            let secret_key = SecretKey::from_seed(&seed, keyset_id, i as u32).unwrap();
 
 
             // Verify the secret key is valid (32 bytes)
             // Verify the secret key is valid (32 bytes)
             let secret_bytes = secret_key.secret_bytes();
             let secret_bytes = secret_key.secret_bytes();
             assert_eq!(secret_bytes.len(), 32);
             assert_eq!(secret_bytes.len(), 32);
 
 
             // Test deterministic derivation
             // Test deterministic derivation
-            let secret_key2 = SecretKey::from_seed(&seed, keyset_id, i).unwrap();
+            let secret_key2 = SecretKey::from_str(test_secret).unwrap();
             assert_eq!(secret_key, secret_key2);
             assert_eq!(secret_key, secret_key2);
         }
         }
     }
     }

+ 1 - 1
crates/cashu/src/nuts/nut18/payment_request.rs

@@ -288,7 +288,7 @@ mod tests {
         assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
         assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
         assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
         assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
 
 
-        let t = request.transports.first().clone().unwrap();
+        let t = request.transports.first().unwrap();
         assert_eq!(&transport, t);
         assert_eq!(&transport, t);
 
 
         // Test serialization and deserialization
         // Test serialization and deserialization

+ 0 - 0
crates/cashu/src/nuts/nut24.rs → crates/cashu/src/nuts/nut25.rs


+ 100 - 0
crates/cashu/src/quote_id.rs

@@ -0,0 +1,100 @@
+//! Quote ID. The specifications only define a string but CDK uses Uuid, so we use an enum to port compatibility.
+use std::fmt;
+use std::str::FromStr;
+
+use bitcoin::base64::engine::general_purpose;
+use bitcoin::base64::Engine as _;
+use serde::{de, Deserialize, Deserializer, Serialize};
+use thiserror::Error;
+use uuid::Uuid;
+
+/// Invalid UUID
+#[derive(Debug, Error)]
+pub enum QuoteIdError {
+    /// UUID Error
+    #[error("invalid UUID: {0}")]
+    Uuid(#[from] uuid::Error),
+    /// Invalid base64
+    #[error("invalid base64")]
+    Base64,
+    /// Invalid quote ID
+    #[error("neither a valid UUID nor a valid base64 string")]
+    InvalidQuoteId,
+}
+
+/// Mint Quote ID
+#[derive(Serialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
+#[serde(untagged)]
+pub enum QuoteId {
+    /// (Nutshell) base64 quote ID
+    BASE64(String),
+    /// UUID quote ID
+    UUID(Uuid),
+}
+
+impl QuoteId {
+    /// Create a new UUID-based MintQuoteId
+    pub fn new_uuid() -> Self {
+        Self::UUID(Uuid::new_v4())
+    }
+}
+
+impl From<Uuid> for QuoteId {
+    fn from(uuid: Uuid) -> Self {
+        Self::UUID(uuid)
+    }
+}
+
+impl fmt::Display for QuoteId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            QuoteId::BASE64(s) => write!(f, "{}", s),
+            QuoteId::UUID(u) => write!(f, "{}", u.hyphenated()),
+        }
+    }
+}
+
+impl FromStr for QuoteId {
+    type Err = QuoteIdError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        // Try UUID first
+        if let Ok(u) = Uuid::parse_str(s) {
+            return Ok(QuoteId::UUID(u));
+        }
+
+        // Try base64: decode, then re-encode and compare to ensure canonical form
+        // Use the standard (URL/filename safe or standard) depending on your needed alphabet.
+        // Here we use standard base64.
+        match general_purpose::URL_SAFE.decode(s) {
+            Ok(_bytes) => Ok(QuoteId::BASE64(s.to_string())),
+            Err(_) => Err(QuoteIdError::InvalidQuoteId),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for QuoteId {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        // Deserialize as plain string first
+        let s = String::deserialize(deserializer)?;
+
+        // Try UUID first
+        if let Ok(u) = Uuid::parse_str(&s) {
+            return Ok(QuoteId::UUID(u));
+        }
+
+        if general_purpose::URL_SAFE.decode(&s).is_ok() {
+            return Ok(QuoteId::BASE64(s));
+        }
+
+        // Neither matched — return a helpful error
+        Err(de::Error::custom(format!(
+            "QuoteId must be either a UUID (e.g. {}) or a valid base64 string; got: {}",
+            Uuid::nil(),
+            s
+        )))
+    }
+}

+ 5 - 4
crates/cdk-cln/src/lib.rs

@@ -19,7 +19,7 @@ use cdk_common::common::FeeReserve;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
 use cdk_common::payment::{
     self, Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
     self, Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
-    CreateIncomingPaymentResponse, IncomingPaymentOptions, MakePaymentResponse, MintPayment,
+    CreateIncomingPaymentResponse, Event, IncomingPaymentOptions, MakePaymentResponse, MintPayment,
     OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, WaitPaymentResponse,
     OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, WaitPaymentResponse,
 };
 };
 use cdk_common::util::{hex, unix_time};
 use cdk_common::util::{hex, unix_time};
@@ -89,9 +89,9 @@ impl MintPayment for Cln {
     }
     }
 
 
     #[instrument(skip_all)]
     #[instrument(skip_all)]
-    async fn wait_any_incoming_payment(
+    async fn wait_payment_event(
         &self,
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
+    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
         tracing::info!(
         tracing::info!(
             "CLN: Starting wait_any_incoming_payment with socket: {:?}",
             "CLN: Starting wait_any_incoming_payment with socket: {:?}",
             self.rpc_socket
             self.rpc_socket
@@ -243,8 +243,9 @@ impl MintPayment for Cln {
                                 payment_id: payment_hash.to_string()
                                 payment_id: payment_hash.to_string()
                             };
                             };
                             tracing::info!("CLN: Created WaitPaymentResponse with amount {} msats", amount_msats.msat());
                             tracing::info!("CLN: Created WaitPaymentResponse with amount {} msats", amount_msats.msat());
+                            let event = Event::PaymentReceived(response);
 
 
-                            break Some((response, (cln_client, last_pay_idx, cancel_token, is_active)));
+                            break Some((event, (cln_client, last_pay_idx, cancel_token, is_active)));
                                 }
                                 }
                                 Err(e) => {
                                 Err(e) => {
                                     tracing::warn!("CLN: Error fetching invoice: {e}");
                                     tracing::warn!("CLN: Error fetching invoice: {e}");

+ 12 - 6
crates/cdk-common/src/database/mint/mod.rs

@@ -5,7 +5,6 @@ use std::collections::HashMap;
 use async_trait::async_trait;
 use async_trait::async_trait;
 use cashu::quote_id::QuoteId;
 use cashu::quote_id::QuoteId;
 use cashu::{Amount, MintInfo};
 use cashu::{Amount, MintInfo};
-use uuid::Uuid;
 
 
 use super::Error;
 use super::Error;
 use crate::common::QuoteTTL;
 use crate::common::QuoteTTL;
@@ -89,7 +88,7 @@ pub trait QuotesTransaction<'a> {
     /// Get [`mint::MeltQuote`] and lock it for update in this transaction
     /// Get [`mint::MeltQuote`] and lock it for update in this transaction
     async fn get_melt_quote(
     async fn get_melt_quote(
         &mut self,
         &mut self,
-        quote_id: &Uuid,
+        quote_id: &QuoteId,
     ) -> Result<Option<mint::MeltQuote>, Self::Err>;
     ) -> Result<Option<mint::MeltQuote>, Self::Err>;
     /// Add [`mint::MeltQuote`]
     /// Add [`mint::MeltQuote`]
     async fn add_melt_quote(&mut self, quote: mint::MeltQuote) -> Result<(), Self::Err>;
     async fn add_melt_quote(&mut self, quote: mint::MeltQuote) -> Result<(), Self::Err>;
@@ -111,7 +110,7 @@ pub trait QuotesTransaction<'a> {
         payment_proof: Option<String>,
         payment_proof: Option<String>,
     ) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err>;
     ) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err>;
     /// Remove [`mint::MeltQuote`]
     /// Remove [`mint::MeltQuote`]
-    async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err>;
+    async fn remove_melt_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err>;
     /// Get all [`MintMintQuote`]s and lock it for update in this transaction
     /// Get all [`MintMintQuote`]s and lock it for update in this transaction
     async fn get_mint_quote_by_request(
     async fn get_mint_quote_by_request(
         &mut self,
         &mut self,
@@ -165,7 +164,11 @@ pub trait ProofsTransaction<'a> {
     ///
     ///
     /// Adds proofs to the database. The database should error if the proof already exits, with a
     /// Adds proofs to the database. The database should error if the proof already exits, with a
     /// `AttemptUpdateSpentProof` if the proof is already spent or a `Duplicate` error otherwise.
     /// `AttemptUpdateSpentProof` if the proof is already spent or a `Duplicate` error otherwise.
-    async fn add_proofs(&mut self, proof: Proofs, quote_id: Option<Uuid>) -> Result<(), Self::Err>;
+    async fn add_proofs(
+        &mut self,
+        proof: Proofs,
+        quote_id: Option<QuoteId>,
+    ) -> Result<(), Self::Err>;
     /// Updates the proofs to a given states and return the previous states
     /// Updates the proofs to a given states and return the previous states
     async fn update_proofs_states(
     async fn update_proofs_states(
         &mut self,
         &mut self,
@@ -177,7 +180,7 @@ pub trait ProofsTransaction<'a> {
     async fn remove_proofs(
     async fn remove_proofs(
         &mut self,
         &mut self,
         ys: &[PublicKey],
         ys: &[PublicKey],
-        quote_id: Option<Uuid>,
+        quote_id: Option<QuoteId>,
     ) -> Result<(), Self::Err>;
     ) -> Result<(), Self::Err>;
 }
 }
 
 
@@ -190,7 +193,10 @@ pub trait ProofsDatabase {
     /// Get [`Proofs`] by ys
     /// Get [`Proofs`] by ys
     async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result<Vec<Option<Proof>>, Self::Err>;
     async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result<Vec<Option<Proof>>, Self::Err>;
     /// Get ys by quote id
     /// Get ys by quote id
-    async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result<Vec<PublicKey>, Self::Err>;
+    async fn get_proof_ys_by_quote_id(
+        &self,
+        quote_id: &QuoteId,
+    ) -> Result<Vec<PublicKey>, Self::Err>;
     /// Get [`Proofs`] state
     /// Get [`Proofs`] state
     async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result<Vec<Option<State>>, Self::Err>;
     async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result<Vec<Option<State>>, Self::Err>;
     /// Get [`Proofs`] by state
     /// Get [`Proofs`] by state

+ 4 - 2
crates/cdk-common/src/database/mint/test.rs

@@ -87,7 +87,7 @@ where
 {
 {
     let keyset_id = setup_keyset(&db).await;
     let keyset_id = setup_keyset(&db).await;
 
 
-    let quote_id = Uuid::max();
+    let quote_id = QuoteId::new_uuid();
 
 
     let proofs = vec![
     let proofs = vec![
         Proof {
         Proof {
@@ -110,7 +110,9 @@ where
 
 
     // Add proofs to database
     // Add proofs to database
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut tx = Database::begin_transaction(&db).await.unwrap();
-    tx.add_proofs(proofs.clone(), Some(quote_id)).await.unwrap();
+    tx.add_proofs(proofs.clone(), Some(quote_id.clone()))
+        .await
+        .unwrap();
     assert!(tx.commit().await.is_ok());
     assert!(tx.commit().await.is_ok());
 
 
     let proofs_from_db = db.get_proofs_by_ys(&[proofs[0].c, proofs[1].c]).await;
     let proofs_from_db = db.get_proofs_by_ys(&[proofs[0].c, proofs[1].c]).await;

+ 9 - 2
crates/cdk-common/src/payment.rs

@@ -295,9 +295,9 @@ pub trait MintPayment {
 
 
     /// Listen for invoices to be paid to the mint
     /// Listen for invoices to be paid to the mint
     /// Returns a stream of request_lookup_id once invoices are paid
     /// Returns a stream of request_lookup_id once invoices are paid
-    async fn wait_any_incoming_payment(
+    async fn wait_payment_event(
         &self,
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err>;
+    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err>;
 
 
     /// Is wait invoice active
     /// Is wait invoice active
     fn is_wait_invoice_active(&self) -> bool;
     fn is_wait_invoice_active(&self) -> bool;
@@ -318,6 +318,13 @@ pub trait MintPayment {
     ) -> Result<MakePaymentResponse, Self::Err>;
     ) -> Result<MakePaymentResponse, Self::Err>;
 }
 }
 
 
+/// An event emitted which should be handled by the mint
+#[derive(Debug, Clone, Hash)]
+pub enum Event {
+    /// A payment has been received.
+    PaymentReceived(WaitPaymentResponse),
+}
+
 /// Wait any invoice response
 /// Wait any invoice response
 #[derive(Debug, Clone, Hash, Serialize, Deserialize)]
 #[derive(Debug, Clone, Hash, Serialize, Deserialize)]
 pub struct WaitPaymentResponse {
 pub struct WaitPaymentResponse {

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

@@ -84,6 +84,9 @@ pub struct MeltQuote {
     pub expiry: u64,
     pub expiry: u64,
     /// Payment preimage
     /// Payment preimage
     pub payment_preimage: Option<String>,
     pub payment_preimage: Option<String>,
+    /// Payment method
+    #[serde(default)]
+    pub payment_method: PaymentMethod,
 }
 }
 
 
 impl MintQuote {
 impl MintQuote {

+ 11 - 8
crates/cdk-fake-wallet/src/lib.rs

@@ -27,7 +27,7 @@ use cdk_common::common::FeeReserve;
 use cdk_common::ensure_cdk;
 use cdk_common::ensure_cdk;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
 use cdk_common::payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
+    self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
     MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
     MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
     PaymentQuoteResponse, WaitPaymentResponse,
     PaymentQuoteResponse, WaitPaymentResponse,
 };
 };
@@ -295,9 +295,9 @@ impl MintPayment for FakeWallet {
     }
     }
 
 
     #[instrument(skip_all)]
     #[instrument(skip_all)]
-    async fn wait_any_incoming_payment(
+    async fn wait_payment_event(
         &self,
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
+    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
         tracing::info!("Starting stream for fake invoices");
         tracing::info!("Starting stream for fake invoices");
         let receiver = self
         let receiver = self
             .receiver
             .receiver
@@ -309,11 +309,14 @@ impl MintPayment for FakeWallet {
         let unit = self.unit.clone();
         let unit = self.unit.clone();
         let receiver_stream = ReceiverStream::new(receiver);
         let receiver_stream = ReceiverStream::new(receiver);
         Ok(Box::pin(receiver_stream.map(
         Ok(Box::pin(receiver_stream.map(
-            move |(request_lookup_id, payment_amount, payment_id)| WaitPaymentResponse {
-                payment_identifier: request_lookup_id.clone(),
-                payment_amount,
-                unit: unit.clone(),
-                payment_id,
+            move |(request_lookup_id, payment_amount, payment_id)| {
+                let wait_response = WaitPaymentResponse {
+                    payment_identifier: request_lookup_id.clone(),
+                    payment_amount,
+                    unit: unit.clone(),
+                    payment_id,
+                };
+                Event::PaymentReceived(wait_response)
             },
             },
         )))
         )))
     }
     }

+ 18 - 14
crates/cdk-integration-tests/tests/bolt12.rs

@@ -1,13 +1,14 @@
 use std::env;
 use std::env;
 use std::path::PathBuf;
 use std::path::PathBuf;
+use std::str::FromStr;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
 use anyhow::{bail, Result};
 use anyhow::{bail, Result};
 use bip39::Mnemonic;
 use bip39::Mnemonic;
 use cashu::amount::SplitTarget;
 use cashu::amount::SplitTarget;
 use cashu::nut23::Amountless;
 use cashu::nut23::Amountless;
-use cashu::{Amount, CurrencyUnit, MintRequest, PreMintSecrets, ProofsMethods};
-use cdk::wallet::{HttpClient, MintConnector, Wallet};
+use cashu::{Amount, CurrencyUnit, MintRequest, MintUrl, PreMintSecrets, ProofsMethods};
+use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletBuilder};
 use cdk_integration_tests::get_mint_url_from_env;
 use cdk_integration_tests::get_mint_url_from_env;
 use cdk_integration_tests::init_regtest::{get_cln_dir, get_temp_dir};
 use cdk_integration_tests::init_regtest::{get_cln_dir, get_temp_dir};
 use cdk_sqlite::wallet::memory;
 use cdk_sqlite::wallet::memory;
@@ -97,13 +98,16 @@ async fn test_regtest_bolt12_mint() {
 /// - Tests the functionality of reusing a quote for multiple payments
 /// - Tests the functionality of reusing a quote for multiple payments
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
 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_url = MintUrl::from_str(&get_mint_url_from_env())?;
+
+    let wallet = WalletBuilder::new()
+        .mint_url(mint_url)
+        .unit(CurrencyUnit::Sat)
+        .localstore(Arc::new(memory::empty().await?))
+        .seed(Mnemonic::generate(12)?.to_seed_normalized(""))
+        .target_proof_count(3)
+        .use_http_subscription()
+        .build()?;
 
 
     let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
     let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
 
 
@@ -120,7 +124,7 @@ async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await?;
         .await?;
 
 
@@ -136,7 +140,7 @@ async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await?;
         .await?;
 
 
@@ -187,7 +191,7 @@ async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
             quote_one.clone(),
             quote_one.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await?;
         .await?;
 
 
@@ -206,7 +210,7 @@ async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
             quote_two.clone(),
             quote_two.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await?;
         .await?;
 
 
@@ -283,7 +287,7 @@ async fn test_regtest_bolt12_melt() -> Result<()> {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await?;
         .await?;
 
 

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

@@ -336,7 +336,7 @@ async fn test_mint_with_auth() {
             quote.clone(),
             quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");

+ 5 - 5
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -114,7 +114,7 @@ async fn test_happy_mint_melt_round_trip() {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");
@@ -236,7 +236,7 @@ async fn test_happy_mint() {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");
@@ -284,7 +284,7 @@ async fn test_restore() {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");
@@ -364,7 +364,7 @@ async fn test_fake_melt_change_in_quote() {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");
@@ -434,7 +434,7 @@ async fn test_pay_invoice_twice() {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");

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

@@ -10,7 +10,7 @@ async fn test_ldk_node_mint_info() -> Result<()> {
     let client = reqwest::Client::new();
     let client = reqwest::Client::new();
 
 
     // Make a request to the info endpoint
     // Make a request to the info endpoint
-    let response = client.get(&format!("{}/v1/info", mint_url)).send().await?;
+    let response = client.get(format!("{}/v1/info", mint_url)).send().await?;
 
 
     // Check that we got a successful response
     // Check that we got a successful response
     assert_eq!(response.status(), 200);
     assert_eq!(response.status(), 200);
@@ -44,7 +44,7 @@ async fn test_ldk_node_mint_quote() -> Result<()> {
 
 
     // Make a request to create a mint quote
     // Make a request to create a mint quote
     let response = client
     let response = client
-        .post(&format!("{}/v1/mint/quote/bolt11", mint_url))
+        .post(format!("{}/v1/mint/quote/bolt11", mint_url))
         .json(&quote_request)
         .json(&quote_request)
         .send()
         .send()
         .await?;
         .await?;

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

@@ -56,7 +56,7 @@ async fn test_internal_payment() {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");
@@ -88,7 +88,7 @@ async fn test_internal_payment() {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");
@@ -236,7 +236,7 @@ async fn test_multimint_melt() {
             quote.clone(),
             quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");
@@ -252,7 +252,7 @@ async fn test_multimint_melt() {
             quote.clone(),
             quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");

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

@@ -32,7 +32,7 @@ async fn test_swap() {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");
@@ -92,7 +92,7 @@ async fn test_fake_melt_change_in_quote() {
             mint_quote.clone(),
             mint_quote.clone(),
             SplitTarget::default(),
             SplitTarget::default(),
             None,
             None,
-            tokio::time::Duration::from_secs(15),
+            tokio::time::Duration::from_secs(60),
         )
         )
         .await
         .await
         .expect("payment");
         .expect("payment");

+ 1 - 4
crates/cdk-ldk-node/Cargo.toml

@@ -15,7 +15,7 @@ async-trait.workspace = true
 axum.workspace = true
 axum.workspace = true
 cdk-common = { workspace = true, features = ["mint"] }
 cdk-common = { workspace = true, features = ["mint"] }
 futures.workspace = true
 futures.workspace = true
-tokio.workspace = true 
+tokio.workspace = true
 tokio-util.workspace = true
 tokio-util.workspace = true
 tracing.workspace = true
 tracing.workspace = true
 thiserror.workspace = true
 thiserror.workspace = true
@@ -29,6 +29,3 @@ tower-http.workspace = true
 rust-embed = "8.5.0"
 rust-embed = "8.5.0"
 serde_urlencoded = "0.7"
 serde_urlencoded = "0.7"
 urlencoding = "2.1"
 urlencoding = "2.1"
-
-
-

+ 4 - 4
crates/cdk-ldk-node/src/lib.rs

@@ -823,9 +823,9 @@ impl MintPayment for CdkLdkNode {
     /// Listen for invoices to be paid to the mint
     /// Listen for invoices to be paid to the mint
     /// Returns a stream of request_lookup_id once invoices are paid
     /// Returns a stream of request_lookup_id once invoices are paid
     #[instrument(skip(self))]
     #[instrument(skip(self))]
-    async fn wait_any_incoming_payment(
+    async fn wait_payment_event(
         &self,
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
+    ) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
         tracing::info!("Starting stream for invoices - wait_any_incoming_payment called");
         tracing::info!("Starting stream for invoices - wait_any_incoming_payment called");
 
 
         // Set active flag to indicate stream is active
         // Set active flag to indicate stream is active
@@ -839,10 +839,10 @@ impl MintPayment for CdkLdkNode {
         // Transform the String stream into a WaitPaymentResponse stream
         // Transform the String stream into a WaitPaymentResponse stream
         let response_stream = BroadcastStream::new(receiver.resubscribe());
         let response_stream = BroadcastStream::new(receiver.resubscribe());
 
 
-        // Map the stream to handle BroadcastStreamRecvError
+        // Map the stream to handle BroadcastStreamRecvError and wrap in Event
         let response_stream = response_stream.filter_map(|result| async move {
         let response_stream = response_stream.filter_map(|result| async move {
             match result {
             match result {
-                Ok(payment) => Some(payment),
+                Ok(payment) => Some(cdk_common::payment::Event::PaymentReceived(payment)),
                 Err(err) => {
                 Err(err) => {
                     tracing::warn!("Error in broadcast stream: {}", err);
                     tracing::warn!("Error in broadcast stream: {}", err);
                     None
                     None

+ 61 - 29
crates/cdk-ldk-node/src/web/handlers/channels.rs

@@ -213,7 +213,7 @@ pub async fn post_open_channel(
 }
 }
 
 
 pub async fn close_channel_page(
 pub async fn close_channel_page(
-    State(_state): State<AppState>,
+    State(state): State<AppState>,
     query: Query<HashMap<String, String>>,
     query: Query<HashMap<String, String>>,
 ) -> Result<Html<String>, StatusCode> {
 ) -> Result<Html<String>, StatusCode> {
     let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
     let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
@@ -229,24 +229,40 @@ pub async fn close_channel_page(
         return Ok(Html(layout("Close Channel Error", content).into_string()));
         return Ok(Html(layout("Close Channel Error", content).into_string()));
     }
     }
 
 
+    // Get channel information for amount display
+    let channels = state.node.inner.list_channels();
+    let channel = channels
+        .iter()
+        .find(|c| c.user_channel_id.0.to_string() == channel_id);
+
     let content = form_card(
     let content = form_card(
         "Close Channel",
         "Close Channel",
         html! {
         html! {
-            p { "Are you sure you want to close this channel?" }
-            div class="info-item" {
-                span class="info-label" { "User Channel ID:" }
-                span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel_id) }
-            }
-            div class="info-item" {
-                span class="info-label" { "Node ID:" }
-                span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (node_id) }
+            p style="margin-bottom: 1.5rem;" { "Are you sure you want to close this channel?" }
+
+            // Channel details in consistent format
+            div class="channel-details" {
+                div class="detail-row" {
+                    span class="detail-label" { "User Channel ID" }
+                    span class="detail-value-amount" { (channel_id) }
+                }
+                div class="detail-row" {
+                    span class="detail-label" { "Node ID" }
+                    span class="detail-value-amount" { (node_id) }
+                }
+                @if let Some(ch) = channel {
+                    div class="detail-row" {
+                        span class="detail-label" { "Channel Amount" }
+                        span class="detail-value-amount" { (format_sats_as_btc(ch.channel_value_sats)) }
+                    }
+                }
             }
             }
-            form method="post" action="/channels/close" style="margin-top: 1rem;" {
+
+            form method="post" action="/channels/close" style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center;" {
                 input type="hidden" name="channel_id" value=(channel_id) {}
                 input type="hidden" name="channel_id" value=(channel_id) {}
                 input type="hidden" name="node_id" value=(node_id) {}
                 input type="hidden" name="node_id" value=(node_id) {}
-                button type="submit" style="background: #dc3545;" { "Close Channel" }
-                " "
-                a href="/balance" { button type="button" { "Cancel" } }
+                a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
+                button type="submit" class="button-destructive" { "Close Channel" }
             }
             }
         },
         },
     );
     );
@@ -255,7 +271,7 @@ pub async fn close_channel_page(
 }
 }
 
 
 pub async fn force_close_channel_page(
 pub async fn force_close_channel_page(
-    State(_state): State<AppState>,
+    State(state): State<AppState>,
     query: Query<HashMap<String, String>>,
     query: Query<HashMap<String, String>>,
 ) -> Result<Html<String>, StatusCode> {
 ) -> Result<Html<String>, StatusCode> {
     let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
     let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
@@ -273,32 +289,48 @@ pub async fn force_close_channel_page(
         ));
         ));
     }
     }
 
 
+    // Get channel information for amount display
+    let channels = state.node.inner.list_channels();
+    let channel = channels
+        .iter()
+        .find(|c| c.user_channel_id.0.to_string() == channel_id);
+
     let content = form_card(
     let content = form_card(
         "Force Close Channel",
         "Force Close Channel",
         html! {
         html! {
-            div style="border: 2px solid #d63384; background-color: rgba(214, 51, 132, 0.1); padding: 1rem; margin-bottom: 1rem; border-radius: 0.5rem;" {
-                h4 style="color: #d63384; margin: 0 0 0.5rem 0;" { "⚠️ Warning: Force Close" }
-                p style="color: #d63384; margin: 0; font-size: 0.9rem;" {
+            div style="border: 2px solid #f97316; background-color: rgba(249, 115, 22, 0.1); padding: 1rem; margin-bottom: 1rem; border-radius: 0.5rem;" {
+                h4 style="color: #f97316; margin: 0 0 0.5rem 0;" { "⚠️ Warning: Force Close" }
+                p style="color: #f97316; margin: 0; font-size: 0.9rem;" {
                     "Force close should NOT be used if normal close is preferred. "
                     "Force close should NOT be used if normal close is preferred. "
                     "Force close will immediately broadcast the latest commitment transaction and may result in delayed fund recovery. "
                     "Force close will immediately broadcast the latest commitment transaction and may result in delayed fund recovery. "
                     "Only use this if the channel counterparty is unresponsive or there are other issues preventing normal closure."
                     "Only use this if the channel counterparty is unresponsive or there are other issues preventing normal closure."
                 }
                 }
             }
             }
-            p { "Are you sure you want to force close this channel?" }
-            div class="info-item" {
-                span class="info-label" { "User Channel ID:" }
-                span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel_id) }
-            }
-            div class="info-item" {
-                span class="info-label" { "Node ID:" }
-                span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (node_id) }
+            p style="margin-bottom: 1.5rem;" { "Are you sure you want to force close this channel?" }
+
+            // Channel details in consistent format
+            div class="channel-details" {
+                div class="detail-row" {
+                    span class="detail-label" { "User Channel ID" }
+                    span class="detail-value-amount" { (channel_id) }
+                }
+                div class="detail-row" {
+                    span class="detail-label" { "Node ID" }
+                    span class="detail-value-amount" { (node_id) }
+                }
+                @if let Some(ch) = channel {
+                    div class="detail-row" {
+                        span class="detail-label" { "Channel Amount" }
+                        span class="detail-value-amount" { (format_sats_as_btc(ch.channel_value_sats)) }
+                    }
+                }
             }
             }
-            form method="post" action="/channels/force-close" style="margin-top: 1rem;" {
+
+            form method="post" action="/channels/force-close" style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center;" {
                 input type="hidden" name="channel_id" value=(channel_id) {}
                 input type="hidden" name="channel_id" value=(channel_id) {}
                 input type="hidden" name="node_id" value=(node_id) {}
                 input type="hidden" name="node_id" value=(node_id) {}
-                button type="submit" style="background: #d63384;" { "Force Close Channel" }
-                " "
-                a href="/balance" { button type="button" { "Cancel" } }
+                a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
+                button type="submit" class="button-destructive" { "Force Close Channel" }
             }
             }
         },
         },
     );
     );

+ 103 - 54
crates/cdk-ldk-node/src/web/handlers/lightning.rs

@@ -3,7 +3,7 @@ use axum::http::StatusCode;
 use axum::response::Html;
 use axum::response::Html;
 use maud::html;
 use maud::html;
 
 
-use crate::web::handlers::AppState;
+use crate::web::handlers::utils::AppState;
 use crate::web::templates::{format_sats_as_btc, layout};
 use crate::web::templates::{format_sats_as_btc, layout};
 
 
 pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
 pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
@@ -26,18 +26,35 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
         html! {
         html! {
             h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
             h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
 
 
-            // Quick Actions section - matching dashboard style
+            // Quick Actions section - individual cards
             div class="card" style="margin-bottom: 2rem;" {
             div class="card" style="margin-bottom: 2rem;" {
                 h2 { "Quick Actions" }
                 h2 { "Quick Actions" }
-                div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                    a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Open Channel" }
+                div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                    // Open Channel Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning Network channel to connect with another node." }
+                        a href="/channels/open" style="text-decoration: none;" {
+                            button class="button-outline" { "Open Channel" }
+                        }
                     }
                     }
-                    a href="/invoices" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Create Invoice" }
+
+                    // Create Invoice Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments from other users or services." }
+                        a href="/invoices" style="text-decoration: none;" {
+                            button class="button-outline" { "Create Invoice" }
+                        }
                     }
                     }
-                    a href="/payments/send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Make Lightning Payment" }
+
+                    // Make Payment Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices. BOLT 11 & 12 supported." }
+                        a href="/invoices" style="text-decoration: none;" {
+                            button class="button-outline" { "Make Payment" }
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -73,18 +90,35 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
         html! {
         html! {
             h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
             h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
 
 
-            // Quick Actions section - matching dashboard style
+            // Quick Actions section - individual cards
             div class="card" style="margin-bottom: 2rem;" {
             div class="card" style="margin-bottom: 2rem;" {
                 h2 { "Quick Actions" }
                 h2 { "Quick Actions" }
-                div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                    a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Open Channel" }
+                div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                    // Open Channel Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning channel by connecting with another node." }
+                        a href="/channels/open" style="text-decoration: none;" {
+                            button class="button-outline" { "Open Channel" }
+                        }
                     }
                     }
-                    a href="/invoices" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Create Invoice" }
+
+                    // Create Invoice Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments." }
+                        a href="/invoices" style="text-decoration: none;" {
+                            button class="button-outline" { "Create Invoice" }
+                        }
                     }
                     }
-                    a href="/payments/send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Make Lightning Payment" }
+
+                    // Make Payment Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices." }
+                        a href="/payments/send" style="text-decoration: none;" {
+                            button class="button-outline" { "Make Payment" }
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -112,57 +146,72 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
                 }
                 }
             }
             }
 
 
-            div class="card" {
-                h2 { "Channel Details" }
+            // Channel Details header (outside card)
+            h2 class="section-header" { "Channel Details" }
+
+            // Channels list
+            @for (index, channel) in channels.iter().enumerate() {
+                @let node_id = channel.counterparty_node_id.to_string();
+                @let channel_number = index + 1;
 
 
-                // Channels list
-                @for channel in &channels {
-                    div class="channel-item" {
-                        div class="channel-header" {
-                            span class="channel-id" { "Channel ID: " (channel.channel_id.to_string()) }
+                div class="channel-box" {
+                    // Channel number as prominent header
+                    div class="channel-alias" { (format!("Channel {}", channel_number)) }
+
+                    // Channel details in left-aligned format
+                    div class="channel-details" {
+                        div class="detail-row" {
+                            span class="detail-label" { "Channel ID" }
+                            span class="detail-value-amount" { (channel.channel_id.to_string()) }
+                        }
+                        @if let Some(short_channel_id) = channel.short_channel_id {
+                            div class="detail-row" {
+                                span class="detail-label" { "Short Channel ID" }
+                                span class="detail-value-amount" { (short_channel_id.to_string()) }
+                            }
+                        }
+                        div class="detail-row" {
+                            span class="detail-label" { "Node ID" }
+                            span class="detail-value-amount" { (node_id) }
+                        }
+                        div class="detail-row" {
+                            span class="detail-label" { "Status" }
                             @if channel.is_usable {
                             @if channel.is_usable {
                                 span class="status-badge status-active" { "Active" }
                                 span class="status-badge status-active" { "Active" }
                             } @else {
                             } @else {
                                 span class="status-badge status-inactive" { "Inactive" }
                                 span class="status-badge status-inactive" { "Inactive" }
                             }
                             }
                         }
                         }
-                        div class="info-item" {
-                            span class="info-label" { "Counterparty" }
-                            span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel.counterparty_node_id.to_string()) }
+                    }
+
+                    // Balance information cards (keeping existing style)
+                    div class="balance-info" {
+                        div class="balance-item" {
+                            div class="balance-amount" { (format_sats_as_btc(channel.outbound_capacity_msat / 1000)) }
+                            div class="balance-label" { "Outbound" }
                         }
                         }
-                        @if let Some(short_channel_id) = channel.short_channel_id {
-                            div class="info-item" {
-                                span class="info-label" { "Short Channel ID" }
-                                span class="info-value" { (short_channel_id.to_string()) }
-                            }
+                        div class="balance-item" {
+                            div class="balance-amount" { (format_sats_as_btc(channel.inbound_capacity_msat / 1000)) }
+                            div class="balance-label" { "Inbound" }
                         }
                         }
-                        div class="balance-info" {
-                            div class="balance-item" {
-                                div class="balance-amount" { (format_sats_as_btc(channel.outbound_capacity_msat / 1000)) }
-                                div class="balance-label" { "Outbound" }
-                            }
-                            div class="balance-item" {
-                                div class="balance-amount" { (format_sats_as_btc(channel.inbound_capacity_msat / 1000)) }
-                                div class="balance-label" { "Inbound" }
-                            }
-                            div class="balance-item" {
-                                div class="balance-amount" { (format_sats_as_btc(channel.channel_value_sats)) }
-                                div class="balance-label" { "Total" }
-                            }
+                        div class="balance-item" {
+                            div class="balance-amount" { (format_sats_as_btc(channel.channel_value_sats)) }
+                            div class="balance-label" { "Total" }
                         }
                         }
-                        @if channel.is_usable {
-                            div style="margin-top: 1rem; display: flex; gap: 0.5rem;" {
-                                a href=(format!("/channels/close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
-                                    button style="background: #dc3545;" { "Close Channel" }
-                                }
-                                a href=(format!("/channels/force-close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
-                                    button style="background: #d63384;" title="Force close should not be used if normal close is preferred. Force close will broadcast the latest commitment transaction immediately." { "Force Close" }
-                                }
+                    }
+
+                    // Action buttons
+                    @if channel.is_usable {
+                        div class="channel-actions" {
+                            a href=(format!("/channels/close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
+                                button class="button-secondary" { "Close Channel" }
+                            }
+                            a href=(format!("/channels/force-close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
+                                button class="button-destructive" title="Force close should not be used if normal close is preferred. Force close will broadcast the latest commitment transaction immediately." { "Force Close" }
                             }
                             }
                         }
                         }
                     }
                     }
                 }
                 }
-
             }
             }
         }
         }
     };
     };

+ 54 - 21
crates/cdk-ldk-node/src/web/handlers/onchain.rs

@@ -79,15 +79,26 @@ pub async fn onchain_page(
     let mut content = html! {
     let mut content = html! {
         h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
         h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
 
 
-        // Quick Actions section - matching dashboard style
+        // Quick Actions section - individual cards
         div class="card" style="margin-bottom: 2rem;" {
         div class="card" style="margin-bottom: 2rem;" {
             h2 { "Quick Actions" }
             h2 { "Quick Actions" }
-            div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                    button class="button-primary" style="width: 100%;" { "Receive Bitcoin" }
+            div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                // Receive Bitcoin Card
+                div class="quick-action-card" {
+                    h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
+                    p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
+                    a href="/onchain?action=receive" style="text-decoration: none;" {
+                        button class="button-outline" { "Receive Bitcoin" }
+                    }
                 }
                 }
-                a href="/onchain?action=send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                    button class="button-primary" style="width: 100%;" { "Send Bitcoin" }
+
+                // Send Bitcoin Card
+                div class="quick-action-card" {
+                    h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
+                    p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
+                    a href="/onchain?action=send" style="text-decoration: none;" {
+                        button class="button-outline" { "Send Bitcoin" }
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -113,15 +124,26 @@ pub async fn onchain_page(
             content = html! {
             content = html! {
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
 
 
-                // Quick Actions section - matching dashboard style
+                // Quick Actions section - individual cards
                 div class="card" style="margin-bottom: 2rem;" {
                 div class="card" style="margin-bottom: 2rem;" {
                     h2 { "Quick Actions" }
                     h2 { "Quick Actions" }
-                    div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                        a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                            button class="button-primary" style="width: 100%;" { "Receive Bitcoin" }
+                    div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                        // Receive Bitcoin Card
+                        div class="quick-action-card" {
+                            h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
+                            p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
+                            a href="/onchain?action=receive" style="text-decoration: none;" {
+                                button class="button-outline" { "Receive Bitcoin" }
+                            }
                         }
                         }
-                        a href="/onchain?action=send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                            button class="button-primary" style="width: 100%;" { "Send Bitcoin" }
+
+                        // Send Bitcoin Card
+                        div class="quick-action-card" {
+                            h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
+                            p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
+                            a href="/onchain?action=send" style="text-decoration: none;" {
+                                button class="button-outline" { "Send Bitcoin" }
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
@@ -141,7 +163,7 @@ pub async fn onchain_page(
                             }
                             }
                             input type="hidden" id="send_action" name="send_action" value="send" {}
                             input type="hidden" id="send_action" name="send_action" value="send" {}
                             div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
                             div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
-                                a href="/onchain" { button type="button" { "Cancel" } }
+                                a href="/onchain" { button type="button" class="button-secondary" { "Cancel" } }
                                 div style="display: flex; gap: 0.5rem;" {
                                 div style="display: flex; gap: 0.5rem;" {
                                     button type="submit" onclick="document.getElementById('send_action').value='send'" { "Send Payment" }
                                     button type="submit" onclick="document.getElementById('send_action').value='send'" { "Send Payment" }
                                     button type="submit" onclick="document.getElementById('send_action').value='send_all'; document.getElementById('amount_sat').value=''" { "Send All" }
                                     button type="submit" onclick="document.getElementById('send_action').value='send_all'; document.getElementById('amount_sat').value=''" { "Send All" }
@@ -171,15 +193,26 @@ pub async fn onchain_page(
             content = html! {
             content = html! {
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
 
 
-                // Quick Actions section - matching dashboard style
+                // Quick Actions section - individual cards
                 div class="card" style="margin-bottom: 2rem;" {
                 div class="card" style="margin-bottom: 2rem;" {
                     h2 { "Quick Actions" }
                     h2 { "Quick Actions" }
-                    div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                        a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                            button class="button-primary" style="width: 100%;" { "Receive Bitcoin" }
+                    div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                        // Receive Bitcoin Card
+                        div class="quick-action-card" {
+                            h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
+                            p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
+                            a href="/onchain?action=receive" style="text-decoration: none;" {
+                                button class="button-outline" { "Receive Bitcoin" }
+                            }
                         }
                         }
-                        a href="/onchain?action=send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                            button class="button-primary" style="width: 100%;" { "Send Bitcoin" }
+
+                        // Send Bitcoin Card
+                        div class="quick-action-card" {
+                            h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
+                            p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
+                            a href="/onchain?action=send" style="text-decoration: none;" {
+                                button class="button-outline" { "Send Bitcoin" }
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
@@ -191,7 +224,7 @@ pub async fn onchain_page(
                         form method="post" action="/onchain/new-address" {
                         form method="post" action="/onchain/new-address" {
                             p style="margin-bottom: 2rem;" { "Click the button below to generate a new Bitcoin address for receiving on-chain payments." }
                             p style="margin-bottom: 2rem;" { "Click the button below to generate a new Bitcoin address for receiving on-chain payments." }
                             div style="display: flex; justify-content: space-between; gap: 1rem;" {
                             div style="display: flex; justify-content: space-between; gap: 1rem;" {
-                                a href="/onchain" { button type="button" { "Cancel" } }
+                                a href="/onchain" { button type="button" class="button-secondary" { "Cancel" } }
                                 button class="button-primary" type="submit" { "Generate New Address" }
                                 button class="button-primary" type="submit" { "Generate New Address" }
                             }
                             }
                         }
                         }
@@ -345,7 +378,7 @@ pub async fn onchain_confirm_page(
         div class="card" {
         div class="card" {
             div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
             div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
                 a href="/onchain?action=send" {
                 a href="/onchain?action=send" {
-                    button type="button" class="button-secondary" { "Cancel" }
+                    button type="button" class="button-secondary" { "Cancel" }
                 }
                 }
                 div style="display: flex; gap: 0.5rem;" {
                 div style="display: flex; gap: 0.5rem;" {
                     a href=(confirmation_url) {
                     a href=(confirmation_url) {

File diff suppressed because it is too large
+ 447 - 144
crates/cdk-ldk-node/src/web/templates/layout.rs


+ 1 - 1
crates/cdk-ldk-node/src/web/templates/payments.rs

@@ -17,7 +17,7 @@ pub fn payment_list_item(
     let status_class = match status {
     let status_class = match status {
         "Succeeded" => "status-active",
         "Succeeded" => "status-active",
         "Failed" => "status-inactive",
         "Failed" => "status-inactive",
-        "Pending" => "status-badge",
+        "Pending" => "status-pending",
         _ => "status-badge",
         _ => "status-badge",
     };
     };
 
 

BIN
crates/cdk-ldk-node/static/images/bg-dark.jpg


+ 4 - 4
crates/cdk-lnbits/src/lib.rs

@@ -15,7 +15,7 @@ use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
 use cdk_common::common::FeeReserve;
 use cdk_common::common::FeeReserve;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
 use cdk_common::payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
+    self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
     MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
     MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
     PaymentQuoteResponse, WaitPaymentResponse,
     PaymentQuoteResponse, WaitPaymentResponse,
 };
 };
@@ -155,9 +155,9 @@ impl MintPayment for LNbits {
         self.wait_invoice_cancel_token.cancel()
         self.wait_invoice_cancel_token.cancel()
     }
     }
 
 
-    async fn wait_any_incoming_payment(
+    async fn wait_payment_event(
         &self,
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
+    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
         let api = self.lnbits_api.clone();
         let api = self.lnbits_api.clone();
         let cancel_token = self.wait_invoice_cancel_token.clone();
         let cancel_token = self.wait_invoice_cancel_token.clone();
         let is_active = Arc::clone(&self.wait_invoice_is_active);
         let is_active = Arc::clone(&self.wait_invoice_is_active);
@@ -179,7 +179,7 @@ impl MintPayment for LNbits {
                     msg_option = receiver.recv() => {
                     msg_option = receiver.recv() => {
                         Self::process_message(msg_option, &api, &is_active)
                         Self::process_message(msg_option, &api, &is_active)
                             .await
                             .await
-                            .map(|response| (response, (api, cancel_token, is_active)))
+                            .map(|response| (Event::PaymentReceived(response), (api, cancel_token, is_active)))
                     }
                     }
                 }
                 }
             },
             },

+ 5 - 4
crates/cdk-lnd/src/lib.rs

@@ -20,7 +20,7 @@ use cdk_common::bitcoin::hashes::Hash;
 use cdk_common::common::FeeReserve;
 use cdk_common::common::FeeReserve;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
 use cdk_common::payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
+    self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
     MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
     MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
     PaymentQuoteResponse, WaitPaymentResponse,
     PaymentQuoteResponse, WaitPaymentResponse,
 };
 };
@@ -137,9 +137,9 @@ impl MintPayment for Lnd {
     }
     }
 
 
     #[instrument(skip_all)]
     #[instrument(skip_all)]
-    async fn wait_any_incoming_payment(
+    async fn wait_payment_event(
         &self,
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
+    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
         let mut lnd_client = self.lnd_client.clone();
         let mut lnd_client = self.lnd_client.clone();
 
 
         let stream_req = lnrpc::InvoiceSubscription {
         let stream_req = lnrpc::InvoiceSubscription {
@@ -195,7 +195,8 @@ impl MintPayment for Lnd {
                                             };
                                             };
                                             tracing::info!("LND: Created WaitPaymentResponse with amount {} msat", 
                                             tracing::info!("LND: Created WaitPaymentResponse with amount {} msat", 
                                                          msg.amt_paid_msat);
                                                          msg.amt_paid_msat);
-                                            Some((wait_response, (stream, cancel_token, is_active)))
+                                            let event = Event::PaymentReceived(wait_response);
+                                            Some((event, (stream, cancel_token, is_active)))
                             }  else { None }
                             }  else { None }
                         } else {
                         } else {
                             None
                             None

+ 5 - 3
crates/cdk-payment-processor/src/proto/client.rs

@@ -263,9 +263,9 @@ impl MintPayment for PaymentProcessorClient {
     }
     }
 
 
     #[instrument(skip_all)]
     #[instrument(skip_all)]
-    async fn wait_any_incoming_payment(
+    async fn wait_payment_event(
         &self,
         &self,
-    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
+    ) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
         self.wait_incoming_payment_stream_is_active
         self.wait_incoming_payment_stream_is_active
             .store(true, Ordering::SeqCst);
             .store(true, Ordering::SeqCst);
         tracing::debug!("Client waiting for payment");
         tracing::debug!("Client waiting for payment");
@@ -288,7 +288,9 @@ impl MintPayment for PaymentProcessorClient {
             .filter_map(|item| async {
             .filter_map(|item| async {
                 match item {
                 match item {
                     Ok(value) => match value.try_into() {
                     Ok(value) => match value.try_into() {
-                        Ok(payment_response) => Some(payment_response),
+                        Ok(payment_response) => Some(cdk_common::payment::Event::PaymentReceived(
+                            payment_response,
+                        )),
                         Err(e) => {
                         Err(e) => {
                             tracing::error!("Error converting payment response: {}", e);
                             tracing::error!("Error converting payment response: {}", e);
                             None
                             None

+ 15 - 11
crates/cdk-payment-processor/src/proto/server.rs

@@ -401,19 +401,23 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
                         ln.cancel_wait_invoice();
                         ln.cancel_wait_invoice();
                         break;
                         break;
                     }
                     }
-                    result = ln.wait_any_incoming_payment() => {
+                    result = ln.wait_payment_event() => {
                         match result {
                         match result {
                             Ok(mut stream) => {
                             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;
+                                while let Some(event) = stream.next().await {
+                                    match event {
+                                        cdk_common::payment::Event::PaymentReceived(payment_response) => {
+                                            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;
+                                                }
+                                            }
                                         }
                                         }
                                     }
                                     }
                                 }
                                 }

+ 1 - 1
crates/cdk-postgres/Cargo.toml

@@ -8,7 +8,6 @@ license.workspace = true
 homepage = "https://github.com/cashubtc/cdk"
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
 repository = "https://github.com/cashubtc/cdk.git"
 rust-version.workspace = true                            # MSRV
 rust-version.workspace = true                            # MSRV
-readme = "README.md"
 
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [features]
 [features]
@@ -32,4 +31,5 @@ uuid.workspace = true
 tokio-postgres = "0.7.13"
 tokio-postgres = "0.7.13"
 futures-util = "0.3.31"
 futures-util = "0.3.31"
 postgres-native-tls = "0.5.1"
 postgres-native-tls = "0.5.1"
+native-tls = "0.2"
 once_cell.workspace = true
 once_cell.workspace = true

+ 44 - 3
crates/cdk-postgres/src/lib.rs

@@ -10,6 +10,8 @@ use cdk_sql_common::pool::{DatabaseConfig, DatabasePool};
 use cdk_sql_common::stmt::{Column, Statement};
 use cdk_sql_common::stmt::{Column, Statement};
 use cdk_sql_common::{SQLMintDatabase, SQLWalletDatabase};
 use cdk_sql_common::{SQLMintDatabase, SQLWalletDatabase};
 use db::{pg_batch, pg_execute, pg_fetch_all, pg_fetch_one, pg_pluck};
 use db::{pg_batch, pg_execute, pg_fetch_all, pg_fetch_one, pg_pluck};
+use native_tls::TlsConnector;
+use postgres_native_tls::MakeTlsConnector;
 use tokio::sync::{Mutex, Notify};
 use tokio::sync::{Mutex, Notify};
 use tokio::time::timeout;
 use tokio::time::timeout;
 use tokio_postgres::{connect, Client, Error as PgError, NoTls};
 use tokio_postgres::{connect, Client, Error as PgError, NoTls};
@@ -25,6 +27,11 @@ pub enum SslMode {
     NoTls(NoTls),
     NoTls(NoTls),
     NativeTls(postgres_native_tls::MakeTlsConnector),
     NativeTls(postgres_native_tls::MakeTlsConnector),
 }
 }
+const SSLMODE_VERIFY_FULL: &str = "sslmode=verify-full";
+const SSLMODE_VERIFY_CA: &str = "sslmode=verify-ca";
+const SSLMODE_PREFER: &str = "sslmode=prefer";
+const SSLMODE_ALLOW: &str = "sslmode=allow";
+const SSLMODE_REQUIRE: &str = "sslmode=require";
 
 
 impl Default for SslMode {
 impl Default for SslMode {
     fn default() -> Self {
     fn default() -> Self {
@@ -61,10 +68,44 @@ impl DatabaseConfig for PgConfig {
 }
 }
 
 
 impl From<&str> for PgConfig {
 impl From<&str> for PgConfig {
-    fn from(value: &str) -> Self {
+    fn from(conn_str: &str) -> Self {
+        fn build_tls(accept_invalid_certs: bool, accept_invalid_hostnames: bool) -> SslMode {
+            let mut builder = TlsConnector::builder();
+            if accept_invalid_certs {
+                builder.danger_accept_invalid_certs(true);
+            }
+            if accept_invalid_hostnames {
+                builder.danger_accept_invalid_hostnames(true);
+            }
+
+            match builder.build() {
+                Ok(connector) => {
+                    let make_tls_connector = MakeTlsConnector::new(connector);
+                    SslMode::NativeTls(make_tls_connector)
+                }
+                Err(_) => SslMode::NoTls(NoTls {}),
+            }
+        }
+
+        let tls = if conn_str.contains(SSLMODE_VERIFY_FULL) {
+            // Strict TLS: valid certs and hostnames required
+            build_tls(false, false)
+        } else if conn_str.contains(SSLMODE_VERIFY_CA) {
+            // Verify CA, but allow invalid hostnames
+            build_tls(false, true)
+        } else if conn_str.contains(SSLMODE_PREFER)
+            || conn_str.contains(SSLMODE_ALLOW)
+            || conn_str.contains(SSLMODE_REQUIRE)
+        {
+            // Lenient TLS for preferred/allow/require: accept invalid certs and hostnames
+            build_tls(true, true)
+        } else {
+            SslMode::NoTls(NoTls {})
+        };
+
         PgConfig {
         PgConfig {
-            url: value.to_owned(),
-            tls: Default::default(),
+            url: conn_str.to_owned(),
+            tls,
         }
         }
     }
     }
 }
 }

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

@@ -27,5 +27,4 @@ tokio.workspace = true
 serde.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 serde_json.workspace = true
 lightning-invoice.workspace = true
 lightning-invoice.workspace = true
-uuid.workspace = true
 once_cell.workspace = true
 once_cell.workspace = true

+ 29 - 123
crates/cdk-sql-common/src/mint/mod.rs

@@ -37,7 +37,6 @@ use cdk_common::{
 use lightning_invoice::Bolt11Invoice;
 use lightning_invoice::Bolt11Invoice;
 use migrations::MIGRATIONS;
 use migrations::MIGRATIONS;
 use tracing::instrument;
 use tracing::instrument;
-use uuid::Uuid;
 
 
 use crate::common::migrate;
 use crate::common::migrate;
 use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
 use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
@@ -170,7 +169,7 @@ where
     async fn add_proofs(
     async fn add_proofs(
         &mut self,
         &mut self,
         proofs: Proofs,
         proofs: Proofs,
-        quote_id: Option<Uuid>,
+        quote_id: Option<QuoteId>,
     ) -> Result<(), Self::Err> {
     ) -> Result<(), Self::Err> {
         let current_time = unix_time();
         let current_time = unix_time();
 
 
@@ -213,7 +212,7 @@ where
                 proof.witness.map(|w| serde_json::to_string(&w).unwrap()),
                 proof.witness.map(|w| serde_json::to_string(&w).unwrap()),
             )
             )
             .bind("state", "UNSPENT".to_string())
             .bind("state", "UNSPENT".to_string())
-            .bind("quote_id", quote_id.map(|q| q.hyphenated().to_string()))
+            .bind("quote_id", quote_id.clone().map(|q| q.to_string()))
             .bind("created_time", current_time as i64)
             .bind("created_time", current_time as i64)
             .execute(&self.inner)
             .execute(&self.inner)
             .await?;
             .await?;
@@ -254,7 +253,7 @@ where
     async fn remove_proofs(
     async fn remove_proofs(
         &mut self,
         &mut self,
         ys: &[PublicKey],
         ys: &[PublicKey],
-        _quote_id: Option<Uuid>,
+        _quote_id: Option<QuoteId>,
     ) -> Result<(), Self::Err> {
     ) -> Result<(), Self::Err> {
         if ys.is_empty() {
         if ys.is_empty() {
             return Ok(());
             return Ok(());
@@ -328,13 +327,7 @@ where
             quote_id=:quote_id
             quote_id=:quote_id
         "#,
         "#,
     )?
     )?
-    .bind(
-        "quote_id",
-        match quote_id {
-            QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-            QuoteId::BASE64(s) => s.to_string(),
-        },
-    )
+    .bind("quote_id", quote_id.to_string())
     .fetch_all(conn)
     .fetch_all(conn)
     .await?
     .await?
     .into_iter()
     .into_iter()
@@ -363,13 +356,7 @@ FROM mint_quote_issued
 WHERE quote_id=:quote_id
 WHERE quote_id=:quote_id
             "#,
             "#,
     )?
     )?
-    .bind(
-        "quote_id",
-        match quote_id {
-            QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-            QuoteId::BASE64(s) => s.to_string(),
-        },
-    )
+    .bind("quote_id", quote_id.to_string())
     .fetch_all(conn)
     .fetch_all(conn)
     .await?
     .await?
     .into_iter()
     .into_iter()
@@ -591,13 +578,7 @@ where
             FOR UPDATE
             FOR UPDATE
             "#,
             "#,
         )?
         )?
-        .bind(
-            "quote_id",
-            match quote_id {
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                QuoteId::BASE64(s) => s.to_string(),
-            },
-        )
+        .bind("quote_id", quote_id.to_string())
         .fetch_one(&self.inner)
         .fetch_one(&self.inner)
         .await
         .await
         .inspect_err(|err| {
         .inspect_err(|err| {
@@ -632,13 +613,7 @@ where
             "#,
             "#,
         )?
         )?
         .bind("amount_paid", new_amount_paid.to_i64())
         .bind("amount_paid", new_amount_paid.to_i64())
-        .bind(
-            "quote_id",
-            match quote_id {
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                QuoteId::BASE64(s) => s.to_string(),
-            },
-        )
+        .bind("quote_id", quote_id.to_string())
         .execute(&self.inner)
         .execute(&self.inner)
         .await
         .await
         .inspect_err(|err| {
         .inspect_err(|err| {
@@ -653,13 +628,7 @@ where
             VALUES (:quote_id, :payment_id, :amount, :timestamp)
             VALUES (:quote_id, :payment_id, :amount, :timestamp)
             "#,
             "#,
         )?
         )?
-        .bind(
-            "quote_id",
-            match quote_id {
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                QuoteId::BASE64(s) => s.to_string(),
-            },
-        )
+        .bind("quote_id", quote_id.to_string())
         .bind("payment_id", payment_id)
         .bind("payment_id", payment_id)
         .bind("amount", amount_paid.to_i64())
         .bind("amount", amount_paid.to_i64())
         .bind("timestamp", unix_time() as i64)
         .bind("timestamp", unix_time() as i64)
@@ -688,13 +657,7 @@ where
             FOR UPDATE
             FOR UPDATE
             "#,
             "#,
         )?
         )?
-        .bind(
-            "quote_id",
-            match quote_id {
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                QuoteId::BASE64(s) => s.to_string(),
-            },
-        )
+        .bind("quote_id", quote_id.to_string())
         .fetch_one(&self.inner)
         .fetch_one(&self.inner)
         .await
         .await
         .inspect_err(|err| {
         .inspect_err(|err| {
@@ -722,13 +685,7 @@ where
             "#,
             "#,
         )?
         )?
         .bind("amount_issued", new_amount_issued.to_i64())
         .bind("amount_issued", new_amount_issued.to_i64())
-        .bind(
-            "quote_id",
-            match quote_id {
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                QuoteId::BASE64(s) => s.to_string(),
-            },
-        )
+        .bind("quote_id", quote_id.to_string())
         .execute(&self.inner)
         .execute(&self.inner)
         .await
         .await
         .inspect_err(|err| {
         .inspect_err(|err| {
@@ -744,13 +701,7 @@ INSERT INTO mint_quote_issued
 VALUES (:quote_id, :amount, :timestamp);
 VALUES (:quote_id, :amount, :timestamp);
             "#,
             "#,
         )?
         )?
-        .bind(
-            "quote_id",
-            match quote_id {
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                QuoteId::BASE64(s) => s.to_string(),
-            },
-        )
+        .bind("quote_id", quote_id.to_string())
         .bind("amount", amount_issued.to_i64())
         .bind("amount", amount_issued.to_i64())
         .bind("timestamp", current_time as i64)
         .bind("timestamp", current_time as i64)
         .execute(&self.inner)
         .execute(&self.inner)
@@ -792,13 +743,7 @@ VALUES (:quote_id, :amount, :timestamp);
 
 
     async fn remove_mint_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> {
     async fn remove_mint_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> {
         query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
         query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
-            .bind(
-                "id",
-                match quote_id {
-                    QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                    QuoteId::BASE64(s) => s.to_string(),
-                },
-            )
+            .bind("id", quote_id.to_string())
             .execute(&self.inner)
             .execute(&self.inner)
             .await?;
             .await?;
         Ok(())
         Ok(())
@@ -861,10 +806,7 @@ VALUES (:quote_id, :amount, :timestamp);
         query(r#"UPDATE melt_quote SET request_lookup_id = :new_req_id, request_lookup_id_kind = :new_kind WHERE id = :id"#)?
         query(r#"UPDATE melt_quote SET request_lookup_id = :new_req_id, request_lookup_id_kind = :new_kind WHERE id = :id"#)?
             .bind("new_req_id", new_request_lookup_id.to_string())
             .bind("new_req_id", new_request_lookup_id.to_string())
             .bind("new_kind",new_request_lookup_id.kind() )
             .bind("new_kind",new_request_lookup_id.kind() )
-            .bind("id", match quote_id {
-                QuoteId::BASE64(s) => s.to_string(),
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-            })
+            .bind("id", quote_id.to_string())
             .execute(&self.inner)
             .execute(&self.inner)
             .await?;
             .await?;
         Ok(())
         Ok(())
@@ -900,13 +842,7 @@ VALUES (:quote_id, :amount, :timestamp);
                 AND state != :state
                 AND state != :state
             "#,
             "#,
         )?
         )?
-        .bind(
-            "id",
-            match quote_id {
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                QuoteId::BASE64(s) => s.to_string(),
-            },
-        )
+        .bind("id", quote_id.to_string())
         .bind("state", state.to_string())
         .bind("state", state.to_string())
         .fetch_one(&self.inner)
         .fetch_one(&self.inner)
         .await?
         .await?
@@ -920,22 +856,13 @@ VALUES (:quote_id, :amount, :timestamp);
                 .bind("state", state.to_string())
                 .bind("state", state.to_string())
                 .bind("paid_time", current_time as i64)
                 .bind("paid_time", current_time as i64)
                 .bind("payment_preimage", payment_proof)
                 .bind("payment_preimage", payment_proof)
-                .bind("id", match quote_id {
-                    QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                    QuoteId::BASE64(s) => s.to_string(),
-                })
+                .bind("id", quote_id.to_string())
                 .execute(&self.inner)
                 .execute(&self.inner)
                 .await
                 .await
         } else {
         } else {
             query(r#"UPDATE melt_quote SET state = :state WHERE id = :id"#)?
             query(r#"UPDATE melt_quote SET state = :state WHERE id = :id"#)?
                 .bind("state", state.to_string())
                 .bind("state", state.to_string())
-                .bind(
-                    "id",
-                    match quote_id {
-                        QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                        QuoteId::BASE64(s) => s.to_string(),
-                    },
-                )
+                .bind("id", quote_id.to_string())
                 .execute(&self.inner)
                 .execute(&self.inner)
                 .await
                 .await
         };
         };
@@ -954,14 +881,14 @@ VALUES (:quote_id, :amount, :timestamp);
         Ok((old_state, quote))
         Ok((old_state, quote))
     }
     }
 
 
-    async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err> {
+    async fn remove_melt_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> {
         query(
         query(
             r#"
             r#"
             DELETE FROM melt_quote
             DELETE FROM melt_quote
             WHERE id=?
             WHERE id=?
             "#,
             "#,
         )?
         )?
-        .bind("id", quote_id.as_hyphenated().to_string())
+        .bind("id", quote_id.to_string())
         .execute(&self.inner)
         .execute(&self.inner)
         .await?;
         .await?;
 
 
@@ -993,13 +920,7 @@ VALUES (:quote_id, :amount, :timestamp);
             FOR UPDATE
             FOR UPDATE
             "#,
             "#,
         )?
         )?
-        .bind(
-            "id",
-            match quote_id {
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                QuoteId::BASE64(s) => s.to_string(),
-            },
-        )
+        .bind("id", quote_id.to_string())
         .fetch_one(&self.inner)
         .fetch_one(&self.inner)
         .await?
         .await?
         .map(|row| sql_row_to_mint_quote(row, payments, issuance))
         .map(|row| sql_row_to_mint_quote(row, payments, issuance))
@@ -1008,7 +929,7 @@ VALUES (:quote_id, :amount, :timestamp);
 
 
     async fn get_melt_quote(
     async fn get_melt_quote(
         &mut self,
         &mut self,
-        quote_id: &Uuid,
+        quote_id: &QuoteId,
     ) -> Result<Option<mint::MeltQuote>, Self::Err> {
     ) -> Result<Option<mint::MeltQuote>, Self::Err> {
         Ok(query(
         Ok(query(
             r#"
             r#"
@@ -1033,7 +954,7 @@ VALUES (:quote_id, :amount, :timestamp);
                 id=:id
                 id=:id
             "#,
             "#,
         )?
         )?
-        .bind("id", quote_id.as_hyphenated().to_string())
+        .bind("id", quote_id.to_string())
         .fetch_one(&self.inner)
         .fetch_one(&self.inner)
         .await?
         .await?
         .map(sql_row_to_melt_quote)
         .map(sql_row_to_melt_quote)
@@ -1157,13 +1078,7 @@ where
                 mint_quote
                 mint_quote
             WHERE id = :id"#,
             WHERE id = :id"#,
         )?
         )?
-        .bind(
-            "id",
-            match quote_id {
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-                QuoteId::BASE64(s) => s.to_string(),
-            },
-        )
+        .bind("id", quote_id.to_string())
         .fetch_one(&*conn)
         .fetch_one(&*conn)
         .await?
         .await?
         .map(|row| sql_row_to_mint_quote(row, payments, issuance))
         .map(|row| sql_row_to_mint_quote(row, payments, issuance))
@@ -1319,13 +1234,7 @@ where
                 id=:id
                 id=:id
             "#,
             "#,
         )?
         )?
-        .bind(
-            "id",
-            match quote_id {
-                QuoteId::BASE64(s) => s.to_string(),
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-            },
-        )
+        .bind("id", quote_id.to_string())
         .fetch_one(&*conn)
         .fetch_one(&*conn)
         .await?
         .await?
         .map(sql_row_to_melt_quote)
         .map(sql_row_to_melt_quote)
@@ -1406,7 +1315,10 @@ where
         Ok(ys.iter().map(|y| proofs.remove(y)).collect())
         Ok(ys.iter().map(|y| proofs.remove(y)).collect())
     }
     }
 
 
-    async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result<Vec<PublicKey>, Self::Err> {
+    async fn get_proof_ys_by_quote_id(
+        &self,
+        quote_id: &QuoteId,
+    ) -> Result<Vec<PublicKey>, Self::Err> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         Ok(query(
         Ok(query(
             r#"
             r#"
@@ -1422,7 +1334,7 @@ where
                 quote_id = :quote_id
                 quote_id = :quote_id
             "#,
             "#,
         )?
         )?
-        .bind("quote_id", quote_id.as_hyphenated().to_string())
+        .bind("quote_id", quote_id.to_string())
         .fetch_all(&*conn)
         .fetch_all(&*conn)
         .await?
         .await?
         .into_iter()
         .into_iter()
@@ -1661,13 +1573,7 @@ where
                 quote_id=:quote_id
                 quote_id=:quote_id
             "#,
             "#,
         )?
         )?
-        .bind(
-            "quote_id",
-            match quote_id {
-                QuoteId::BASE64(s) => s.to_string(),
-                QuoteId::UUID(u) => u.as_hyphenated().to_string(),
-            },
-        )
+        .bind("quote_id", quote_id.to_string())
         .fetch_all(&*conn)
         .fetch_all(&*conn)
         .await?
         .await?
         .into_iter()
         .into_iter()

+ 2 - 0
crates/cdk-sql-common/src/wallet/migrations.rs

@@ -2,6 +2,7 @@
 /// Auto-generated by build.rs
 /// Auto-generated by build.rs
 pub static MIGRATIONS: &[(&str, &str, &str)] = &[
 pub static MIGRATIONS: &[(&str, &str, &str)] = &[
     ("postgres", "1_initial.sql", include_str!(r#"./migrations/postgres/1_initial.sql"#)),
     ("postgres", "1_initial.sql", include_str!(r#"./migrations/postgres/1_initial.sql"#)),
+    ("postgres", "20250831215438_melt_quote_method.sql", include_str!(r#"./migrations/postgres/20250831215438_melt_quote_method.sql"#)),
     ("sqlite", "1_fix_sqlx_migration.sql", include_str!(r#"./migrations/sqlite/1_fix_sqlx_migration.sql"#)),
     ("sqlite", "1_fix_sqlx_migration.sql", include_str!(r#"./migrations/sqlite/1_fix_sqlx_migration.sql"#)),
     ("sqlite", "20240612132920_init.sql", include_str!(r#"./migrations/sqlite/20240612132920_init.sql"#)),
     ("sqlite", "20240612132920_init.sql", include_str!(r#"./migrations/sqlite/20240612132920_init.sql"#)),
     ("sqlite", "20240618200350_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618200350_quote_state.sql"#)),
     ("sqlite", "20240618200350_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618200350_quote_state.sql"#)),
@@ -22,4 +23,5 @@ pub static MIGRATIONS: &[(&str, &str, &str)] = &[
     ("sqlite", "20250707093445_bolt12.sql", include_str!(r#"./migrations/sqlite/20250707093445_bolt12.sql"#)),
     ("sqlite", "20250707093445_bolt12.sql", include_str!(r#"./migrations/sqlite/20250707093445_bolt12.sql"#)),
     ("sqlite", "20250729111701_keyset_v2_u32.sql", include_str!(r#"./migrations/sqlite/20250729111701_keyset_v2_u32.sql"#)),
     ("sqlite", "20250729111701_keyset_v2_u32.sql", include_str!(r#"./migrations/sqlite/20250729111701_keyset_v2_u32.sql"#)),
     ("sqlite", "20250812084621_keyset_plus_one.sql", include_str!(r#"./migrations/sqlite/20250812084621_keyset_plus_one.sql"#)),
     ("sqlite", "20250812084621_keyset_plus_one.sql", include_str!(r#"./migrations/sqlite/20250812084621_keyset_plus_one.sql"#)),
+    ("sqlite", "20250831215438_melt_quote_method.sql", include_str!(r#"./migrations/sqlite/20250831215438_melt_quote_method.sql"#)),
 ];
 ];

+ 1 - 0
crates/cdk-sql-common/src/wallet/migrations/postgres/20250831215438_melt_quote_method.sql

@@ -0,0 +1 @@
+ALTER TABLE melt_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'bolt11';

+ 1 - 0
crates/cdk-sql-common/src/wallet/migrations/sqlite/20250831215438_melt_quote_method.sql

@@ -0,0 +1 @@
+ALTER TABLE melt_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'bolt11';

+ 18 - 6
crates/cdk-sql-common/src/wallet/mod.rs

@@ -156,7 +156,8 @@ where
                   fee_reserve,
                   fee_reserve,
                   state,
                   state,
                   expiry,
                   expiry,
-                  payment_preimage
+                  payment_preimage,
+                  payment_method
               FROM
               FROM
                   melt_quote
                   melt_quote
               "#,
               "#,
@@ -551,6 +552,9 @@ ON CONFLICT(id) DO UPDATE SET
                 state,
                 state,
                 expiry,
                 expiry,
                 secret_key
                 secret_key
+                payment_method,
+                amount_issued,
+                amount_paid
             FROM
             FROM
                 mint_quote
                 mint_quote
             "#,
             "#,
@@ -579,16 +583,17 @@ ON CONFLICT(id) DO UPDATE SET
         query(
         query(
             r#"
             r#"
 INSERT INTO melt_quote
 INSERT INTO melt_quote
-(id, unit, amount, request, fee_reserve, state, expiry)
+(id, unit, amount, request, fee_reserve, state, expiry, payment_method)
 VALUES
 VALUES
-(:id, :unit, :amount, :request, :fee_reserve, :state, :expiry)
+(:id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_method)
 ON CONFLICT(id) DO UPDATE SET
 ON CONFLICT(id) DO UPDATE SET
     unit = excluded.unit,
     unit = excluded.unit,
     amount = excluded.amount,
     amount = excluded.amount,
     request = excluded.request,
     request = excluded.request,
     fee_reserve = excluded.fee_reserve,
     fee_reserve = excluded.fee_reserve,
     state = excluded.state,
     state = excluded.state,
-    expiry = excluded.expiry
+    expiry = excluded.expiry,
+    payment_method = excluded.payment_method
 ;
 ;
         "#,
         "#,
         )?
         )?
@@ -599,6 +604,7 @@ ON CONFLICT(id) DO UPDATE SET
         .bind("fee_reserve", u64::from(quote.fee_reserve) as i64)
         .bind("fee_reserve", u64::from(quote.fee_reserve) as i64)
         .bind("state", quote.state.to_string())
         .bind("state", quote.state.to_string())
         .bind("expiry", quote.expiry as i64)
         .bind("expiry", quote.expiry as i64)
+        .bind("payment_method", quote.payment_method.to_string())
         .execute(&*conn)
         .execute(&*conn)
         .await?;
         .await?;
 
 
@@ -618,7 +624,8 @@ ON CONFLICT(id) DO UPDATE SET
                 fee_reserve,
                 fee_reserve,
                 state,
                 state,
                 expiry,
                 expiry,
-                payment_preimage
+                payment_preimage,
+                payment_method
             FROM
             FROM
                 melt_quote
                 melt_quote
             WHERE
             WHERE
@@ -1124,13 +1131,17 @@ fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
             fee_reserve,
             fee_reserve,
             state,
             state,
             expiry,
             expiry,
-            payment_preimage
+            payment_preimage,
+            row_method
         ) = row
         ) = row
     );
     );
 
 
     let amount: u64 = column_as_number!(amount);
     let amount: u64 = column_as_number!(amount);
     let fee_reserve: u64 = column_as_number!(fee_reserve);
     let fee_reserve: u64 = column_as_number!(fee_reserve);
 
 
+    let payment_method =
+        PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
+
     Ok(wallet::MeltQuote {
     Ok(wallet::MeltQuote {
         id: column_as_string!(id),
         id: column_as_string!(id),
         amount: Amount::from(amount),
         amount: Amount::from(amount),
@@ -1140,6 +1151,7 @@ fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
         state: column_as_string!(state, MeltQuoteState::from_str),
         state: column_as_string!(state, MeltQuoteState::from_str),
         expiry: column_as_number!(expiry),
         expiry: column_as_number!(expiry),
         payment_preimage: column_as_nullable_string!(payment_preimage),
         payment_preimage: column_as_nullable_string!(payment_preimage),
+        payment_method,
     })
     })
 }
 }
 
 

+ 6 - 0
crates/cdk/Cargo.toml

@@ -106,6 +106,11 @@ required-features = ["wallet", "bip353"]
 [[example]]
 [[example]]
 name = "mint-token-bolt12-with-stream"
 name = "mint-token-bolt12-with-stream"
 required-features = ["wallet"]
 required-features = ["wallet"]
+
+[[example]]
+name = "mint-token-bolt12-with-custom-http"
+required-features = ["wallet"]
+
 [[example]]
 [[example]]
 name = "mint-token-bolt12"
 name = "mint-token-bolt12"
 required-features = ["wallet"]
 required-features = ["wallet"]
@@ -118,6 +123,7 @@ tracing-subscriber.workspace = true
 criterion = "0.6.0"
 criterion = "0.6.0"
 reqwest = { workspace = true }
 reqwest = { workspace = true }
 anyhow.workspace = true
 anyhow.workspace = true
+ureq = { version = "3.1.0", features = ["json"] }
 
 
 
 
 [[bench]]
 [[bench]]

+ 161 - 0
crates/cdk/examples/mint-token-bolt12-with-custom-http.rs

@@ -0,0 +1,161 @@
+use std::str::FromStr;
+use std::sync::Arc;
+use std::time::Duration;
+
+use cdk::error::Error;
+use cdk::nuts::nut00::ProofsMethods;
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::{BaseHttpClient, HttpTransport, SendOptions, WalletBuilder};
+use cdk::{Amount, StreamExt};
+use cdk_common::mint_url::MintUrl;
+use cdk_common::AuthToken;
+use cdk_sqlite::wallet::memory;
+use rand::random;
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use tracing_subscriber::EnvFilter;
+use ureq::config::Config;
+use ureq::Agent;
+use url::Url;
+
+#[derive(Debug, Clone)]
+pub struct CustomHttp {
+    agent: Agent,
+}
+
+impl Default for CustomHttp {
+    fn default() -> Self {
+        Self {
+            agent: Agent::new_with_config(
+                Config::builder()
+                    .timeout_global(Some(Duration::from_secs(5)))
+                    .no_delay(true)
+                    .user_agent("Custom HTTP Transport")
+                    .build(),
+            ),
+        }
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
+impl HttpTransport for CustomHttp {
+    fn with_proxy(
+        &mut self,
+        _proxy: Url,
+        _host_matcher: Option<&str>,
+        _accept_invalid_certs: bool,
+    ) -> Result<(), Error> {
+        panic!("Not supported");
+    }
+
+    async fn http_get<R>(&self, url: Url, _auth: Option<AuthToken>) -> Result<R, Error>
+    where
+        R: DeserializeOwned,
+    {
+        self.agent
+            .get(url.as_str())
+            .call()
+            .map_err(|e| Error::HttpError(None, e.to_string()))?
+            .body_mut()
+            .read_json()
+            .map_err(|e| Error::HttpError(None, e.to_string()))
+    }
+
+    /// HTTP Post request
+    async fn http_post<P, R>(
+        &self,
+        url: Url,
+        _auth_token: Option<AuthToken>,
+        payload: &P,
+    ) -> Result<R, Error>
+    where
+        P: Serialize + ?Sized + Send + Sync,
+        R: DeserializeOwned,
+    {
+        self.agent
+            .post(url.as_str())
+            .send_json(payload)
+            .map_err(|e| Error::HttpError(None, e.to_string()))?
+            .body_mut()
+            .read_json()
+            .map_err(|e| Error::HttpError(None, e.to_string()))
+    }
+}
+
+type CustomConnector = BaseHttpClient<CustomHttp>;
+
+#[tokio::main]
+async fn main() -> Result<(), Error> {
+    let default_filter = "debug";
+
+    let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn,rustls=warn";
+
+    let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
+
+    // Parse input
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
+
+    // Initialize the memory store for the wallet
+    let localstore = Arc::new(memory::empty().await?);
+
+    // Generate a random seed for the wallet
+    let seed = random::<[u8; 64]>();
+
+    // Define the mint URL and currency unit
+    let mint_url = "https://fake.thesimplekid.dev";
+    let unit = CurrencyUnit::Sat;
+    let amount = Amount::from(10);
+
+    let mint_url = MintUrl::from_str(mint_url)?;
+    #[cfg(feature = "auth")]
+    let http_client = CustomConnector::new(mint_url.clone(), None);
+
+    #[cfg(not(feature = "auth"))]
+    let http_client = CustomConnector::new(mint_url.clone());
+
+    // Create a new wallet
+    let wallet = WalletBuilder::new()
+        .mint_url(mint_url)
+        .unit(unit)
+        .localstore(localstore)
+        .seed(seed)
+        .target_proof_count(3)
+        .client(http_client)
+        .build()?;
+
+    let quotes = vec![
+        wallet.mint_bolt12_quote(None, None).await?,
+        wallet.mint_bolt12_quote(None, None).await?,
+        wallet.mint_bolt12_quote(None, None).await?,
+    ];
+
+    let mut stream = wallet.mints_proof_stream(quotes, Default::default(), None);
+
+    let stop = stream.get_cancel_token();
+
+    let mut processed = 0;
+
+    while let Some(proofs) = stream.next().await {
+        let (mint_quote, proofs) = proofs?;
+
+        // Mint the received amount
+        let receive_amount = proofs.total_amount()?;
+        tracing::info!("Received {} from mint {}", receive_amount, mint_quote.id);
+
+        // Send a token with the specified amount
+        let prepared_send = wallet.prepare_send(amount, SendOptions::default()).await?;
+        let token = prepared_send.confirm(None).await?;
+        tracing::info!("Token: {}", token);
+
+        processed += 1;
+
+        if processed == 3 {
+            stop.cancel()
+        }
+    }
+
+    tracing::info!("Stopped the loop after {} quotes being minted", processed);
+
+    Ok(())
+}

+ 12 - 8
crates/cdk/src/mint/mod.rs

@@ -536,16 +536,20 @@ impl Mint {
                     processor.cancel_wait_invoice();
                     processor.cancel_wait_invoice();
                     break;
                     break;
                 }
                 }
-                result = processor.wait_any_incoming_payment() => {
+                result = processor.wait_payment_event() => {
                     match result {
                     match result {
                         Ok(mut stream) => {
                         Ok(mut stream) => {
-                            while let Some(request_lookup_id) = stream.next().await {
-                                if let Err(e) = Self::handle_payment_notification(
-                                    &localstore,
-                                    &pubsub_manager,
-                                    request_lookup_id,
-                                ).await {
-                                    tracing::warn!("Payment notification error: {:?}", e);
+                            while let Some(event) = stream.next().await {
+                                match event {
+                                    cdk_common::payment::Event::PaymentReceived(wait_payment_response) => {
+                                        if let Err(e) = Self::handle_payment_notification(
+                                            &localstore,
+                                            &pubsub_manager,
+                                            wait_payment_response,
+                                        ).await {
+                                            tracing::warn!("Payment notification error: {:?}", e);
+                                        }
+                                    }
                                 }
                                 }
                             }
                             }
                         }
                         }

+ 39 - 26
crates/cdk/src/pub_sub.rs

@@ -41,10 +41,10 @@ pub struct Manager<T, I, F>
 where
 where
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
     I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
-    F: OnNewSubscription<Index = I, Event = T> + 'static,
+    F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
 {
 {
     indexes: IndexTree<T, I>,
     indexes: IndexTree<T, I>,
-    on_new_subscription: Option<F>,
+    on_new_subscription: Option<Arc<F>>,
     unsubscription_sender: mpsc::Sender<(SubId, Vec<Index<I>>)>,
     unsubscription_sender: mpsc::Sender<(SubId, Vec<Index<I>>)>,
     active_subscriptions: Arc<AtomicUsize>,
     active_subscriptions: Arc<AtomicUsize>,
     background_subscription_remover: Option<JoinHandle<()>>,
     background_subscription_remover: Option<JoinHandle<()>>,
@@ -54,7 +54,7 @@ impl<T, I, F> Default for Manager<T, I, F>
 where
 where
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
     I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
-    F: OnNewSubscription<Index = I, Event = T> + 'static,
+    F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
 {
 {
     fn default() -> Self {
     fn default() -> Self {
         let (sender, receiver) = mpsc::channel(DEFAULT_REMOVE_SIZE);
         let (sender, receiver) = mpsc::channel(DEFAULT_REMOVE_SIZE);
@@ -79,11 +79,11 @@ impl<T, I, F> From<F> for Manager<T, I, F>
 where
 where
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
     I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
-    F: OnNewSubscription<Index = I, Event = T> + 'static,
+    F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
 {
 {
     fn from(value: F) -> Self {
     fn from(value: F) -> Self {
         let mut manager: Self = Default::default();
         let mut manager: Self = Default::default();
-        manager.on_new_subscription = Some(value);
+        manager.on_new_subscription = Some(Arc::new(value));
         manager
         manager
     }
     }
 }
 }
@@ -92,7 +92,7 @@ impl<T, I, F> Manager<T, I, F>
 where
 where
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
     I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
-    F: OnNewSubscription<Index = I, Event = T> + 'static,
+    F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
 {
 {
     #[inline]
     #[inline]
     /// Broadcast an event to all listeners
     /// Broadcast an event to all listeners
@@ -143,32 +143,45 @@ where
         indexes: Vec<Index<I>>,
         indexes: Vec<Index<I>>,
     ) -> ActiveSubscription<T, I> {
     ) -> ActiveSubscription<T, I> {
         let (sender, receiver) = mpsc::channel(10);
         let (sender, receiver) = mpsc::channel(10);
-        if let Some(on_new_subscription) = self.on_new_subscription.as_ref() {
-            match on_new_subscription
-                .on_new_subscription(&indexes.iter().map(|x| x.deref()).collect::<Vec<_>>())
-                .await
-            {
-                Ok(events) => {
-                    for event in events {
-                        let _ = sender.try_send((sub_id.clone(), event));
-                    }
-                }
-                Err(err) => {
-                    tracing::info!(
-                        "Failed to get initial state for subscription: {:?}, {}",
-                        sub_id,
-                        err
-                    );
-                }
-            }
-        }
 
 
         let mut index_storage = self.indexes.write().await;
         let mut index_storage = self.indexes.write().await;
+        // Subscribe to events as soon as possible
         for index in indexes.clone() {
         for index in indexes.clone() {
             index_storage.insert(index, sender.clone());
             index_storage.insert(index, sender.clone());
         }
         }
         drop(index_storage);
         drop(index_storage);
 
 
+        if let Some(on_new_subscription) = self.on_new_subscription.clone() {
+            // After we're subscribed already, fetch the current status of matching events. It is
+            // down in another thread to return right away
+            let indexes_for_worker = indexes.clone();
+            let sub_id_for_worker = sub_id.clone();
+            tokio::spawn(async move {
+                match on_new_subscription
+                    .on_new_subscription(
+                        &indexes_for_worker
+                            .iter()
+                            .map(|x| x.deref())
+                            .collect::<Vec<_>>(),
+                    )
+                    .await
+                {
+                    Ok(events) => {
+                        for event in events {
+                            let _ = sender.try_send((sub_id_for_worker.clone(), event));
+                        }
+                    }
+                    Err(err) => {
+                        tracing::info!(
+                            "Failed to get initial state for subscription: {:?}, {}",
+                            sub_id_for_worker,
+                            err
+                        );
+                    }
+                }
+            });
+        }
+
         self.active_subscriptions
         self.active_subscriptions
             .fetch_add(1, atomic::Ordering::Relaxed);
             .fetch_add(1, atomic::Ordering::Relaxed);
 
 
@@ -232,7 +245,7 @@ impl<T, I, F> Drop for Manager<T, I, F>
 where
 where
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     T: Indexable<Type = I> + Clone + Send + Sync + 'static,
     I: Clone + Debug + PartialOrd + Ord + Send + Sync + 'static,
     I: Clone + Debug + PartialOrd + Ord + Send + Sync + 'static,
-    F: OnNewSubscription<Index = I, Event = T> + 'static,
+    F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
 {
 {
     fn drop(&mut self) {
     fn drop(&mut self) {
         if let Some(handler) = self.background_subscription_remover.take() {
         if let Some(handler) = self.background_subscription_remover.take() {

+ 16 - 1
crates/cdk/src/wallet/builder.rs

@@ -26,6 +26,7 @@ pub struct WalletBuilder {
     #[cfg(feature = "auth")]
     #[cfg(feature = "auth")]
     auth_wallet: Option<AuthWallet>,
     auth_wallet: Option<AuthWallet>,
     seed: Option<[u8; 64]>,
     seed: Option<[u8; 64]>,
+    use_http_subscription: bool,
     client: Option<Arc<dyn MintConnector + Send + Sync>>,
     client: Option<Arc<dyn MintConnector + Send + Sync>>,
 }
 }
 
 
@@ -40,6 +41,7 @@ impl Default for WalletBuilder {
             auth_wallet: None,
             auth_wallet: None,
             seed: None,
             seed: None,
             client: None,
             client: None,
+            use_http_subscription: false,
         }
         }
     }
     }
 }
 }
@@ -50,6 +52,19 @@ impl WalletBuilder {
         Self::default()
         Self::default()
     }
     }
 
 
+    /// Use HTTP for wallet subscriptions to mint events
+    pub fn use_http_subscription(mut self) -> Self {
+        self.use_http_subscription = true;
+        self
+    }
+
+    /// If WS is preferred (with fallback to HTTP is it is not supported by the mint) for the wallet
+    /// subscriptions to mint events
+    pub fn prefer_ws_subscription(mut self) -> Self {
+        self.use_http_subscription = false;
+        self
+    }
+
     /// Set the mint URL
     /// Set the mint URL
     pub fn mint_url(mut self, mint_url: MintUrl) -> Self {
     pub fn mint_url(mut self, mint_url: MintUrl) -> Self {
         self.mint_url = Some(mint_url);
         self.mint_url = Some(mint_url);
@@ -150,7 +165,7 @@ impl WalletBuilder {
             auth_wallet: Arc::new(RwLock::new(self.auth_wallet)),
             auth_wallet: Arc::new(RwLock::new(self.auth_wallet)),
             seed,
             seed,
             client: client.clone(),
             client: client.clone(),
-            subscription: SubscriptionManager::new(client),
+            subscription: SubscriptionManager::new(client, self.use_http_subscription),
         })
         })
     }
     }
 }
 }

+ 1 - 1
crates/cdk/src/wallet/issue/issue_bolt12.rs

@@ -1,7 +1,7 @@
 use std::collections::HashMap;
 use std::collections::HashMap;
 
 
 use cdk_common::nut04::MintMethodOptions;
 use cdk_common::nut04::MintMethodOptions;
-use cdk_common::nut24::MintQuoteBolt12Request;
+use cdk_common::nut25::MintQuoteBolt12Request;
 use cdk_common::wallet::{Transaction, TransactionDirection};
 use cdk_common::wallet::{Transaction, TransactionDirection};
 use cdk_common::{Proofs, SecretKey};
 use cdk_common::{Proofs, SecretKey};
 use tracing::instrument;
 use tracing::instrument;

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

@@ -3,6 +3,7 @@ use std::str::FromStr;
 
 
 use cdk_common::amount::SplitTarget;
 use cdk_common::amount::SplitTarget;
 use cdk_common::wallet::{Transaction, TransactionDirection};
 use cdk_common::wallet::{Transaction, TransactionDirection};
+use cdk_common::PaymentMethod;
 use lightning_invoice::Bolt11Invoice;
 use lightning_invoice::Bolt11Invoice;
 use tracing::instrument;
 use tracing::instrument;
 
 
@@ -87,6 +88,7 @@ impl Wallet {
             state: quote_res.state,
             state: quote_res.state,
             expiry: quote_res.expiry,
             expiry: quote_res.expiry,
             payment_preimage: quote_res.payment_preimage,
             payment_preimage: quote_res.payment_preimage,
+            payment_method: PaymentMethod::Bolt11,
         };
         };
 
 
         self.localstore.add_melt_quote(quote.clone()).await?;
         self.localstore.add_melt_quote(quote.clone()).await?;
@@ -183,7 +185,13 @@ impl Wallet {
             Some(premint_secrets.blinded_messages()),
             Some(premint_secrets.blinded_messages()),
         );
         );
 
 
-        let melt_response = self.client.post_melt(request).await;
+        let melt_response = match quote_info.payment_method {
+            cdk_common::PaymentMethod::Bolt11 => self.client.post_melt(request).await,
+            cdk_common::PaymentMethod::Bolt12 => self.client.post_melt_bolt12(request).await,
+            cdk_common::PaymentMethod::Custom(_) => {
+                return Err(Error::UnsupportedPaymentMethod);
+            }
+        };
 
 
         let melt_response = match melt_response {
         let melt_response = match melt_response {
             Ok(melt_response) => melt_response,
             Ok(melt_response) => melt_response,

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

@@ -6,6 +6,7 @@ use std::str::FromStr;
 
 
 use cdk_common::amount::amount_for_offer;
 use cdk_common::amount::amount_for_offer;
 use cdk_common::wallet::MeltQuote;
 use cdk_common::wallet::MeltQuote;
+use cdk_common::PaymentMethod;
 use lightning::offers::offer::Offer;
 use lightning::offers::offer::Offer;
 use tracing::instrument;
 use tracing::instrument;
 
 
@@ -57,6 +58,7 @@ impl Wallet {
             state: quote_res.state,
             state: quote_res.state,
             expiry: quote_res.expiry,
             expiry: quote_res.expiry,
             payment_preimage: quote_res.payment_preimage,
             payment_preimage: quote_res.payment_preimage,
+            payment_method: PaymentMethod::Bolt12,
         };
         };
 
 
         self.localstore.add_melt_quote(quote.clone()).await?;
         self.localstore.add_melt_quote(quote.clone()).await?;

+ 60 - 156
crates/cdk/src/wallet/mint_connector/http_client.rs

@@ -1,3 +1,4 @@
+//! HTTP Mint client with pluggable transport
 use std::collections::HashSet;
 use std::collections::HashSet;
 use std::sync::{Arc, RwLock as StdRwLock};
 use std::sync::{Arc, RwLock as StdRwLock};
 use std::time::{Duration, Instant};
 use std::time::{Duration, Instant};
@@ -6,17 +7,15 @@ use async_trait::async_trait;
 use cdk_common::{nut19, MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
 use cdk_common::{nut19, MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
 use cdk_common::{Method, ProtectedEndpoint, RoutePath};
 use cdk_common::{Method, ProtectedEndpoint, RoutePath};
-use reqwest::{Client, IntoUrl};
 use serde::de::DeserializeOwned;
 use serde::de::DeserializeOwned;
 use serde::Serialize;
 use serde::Serialize;
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
 use tokio::sync::RwLock;
 use tokio::sync::RwLock;
 use tracing::instrument;
 use tracing::instrument;
-#[cfg(not(target_arch = "wasm32"))]
 use url::Url;
 use url::Url;
 
 
+use super::transport::Transport;
 use super::{Error, MintConnector};
 use super::{Error, MintConnector};
-use crate::error::ErrorResponse;
 use crate::mint_url::MintUrl;
 use crate::mint_url::MintUrl;
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
 use crate::nuts::nut22::MintAuthRequest;
 use crate::nuts::nut22::MintAuthRequest;
@@ -29,119 +28,30 @@ use crate::nuts::{
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
 use crate::wallet::auth::{AuthMintConnector, AuthWallet};
 use crate::wallet::auth::{AuthMintConnector, AuthWallet};
 
 
-#[derive(Debug, Clone)]
-struct HttpClientCore {
-    inner: Client,
-}
-
-impl HttpClientCore {
-    fn new() -> Self {
-        #[cfg(not(target_arch = "wasm32"))]
-        if rustls::crypto::CryptoProvider::get_default().is_none() {
-            let _ = rustls::crypto::ring::default_provider().install_default();
-        }
-
-        Self {
-            inner: Client::new(),
-        }
-    }
-
-    fn client(&self) -> &Client {
-        &self.inner
-    }
-
-    async fn http_get<U: IntoUrl + Send, R: DeserializeOwned>(
-        &self,
-        url: U,
-        auth: Option<AuthToken>,
-    ) -> Result<R, Error> {
-        let mut request = self.client().get(url);
-
-        if let Some(auth) = auth {
-            request = request.header(auth.header_key(), auth.to_string());
-        }
-
-        let response = request
-            .send()
-            .await
-            .map_err(|e| {
-                Error::HttpError(
-                    e.status().map(|status_code| status_code.as_u16()),
-                    e.to_string(),
-                )
-            })?
-            .text()
-            .await
-            .map_err(|e| {
-                Error::HttpError(
-                    e.status().map(|status_code| status_code.as_u16()),
-                    e.to_string(),
-                )
-            })?;
-
-        serde_json::from_str::<R>(&response).map_err(|err| {
-            tracing::warn!("Http Response error: {}", err);
-            match ErrorResponse::from_json(&response) {
-                Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
-                Err(err) => err.into(),
-            }
-        })
-    }
-
-    async fn http_post<U: IntoUrl + Send, P: Serialize + ?Sized, R: DeserializeOwned>(
-        &self,
-        url: U,
-        auth_token: Option<AuthToken>,
-        payload: &P,
-    ) -> Result<R, Error> {
-        let mut request = self.client().post(url).json(&payload);
-
-        if let Some(auth) = auth_token {
-            request = request.header(auth.header_key(), auth.to_string());
-        }
-
-        let response = request.send().await.map_err(|e| {
-            Error::HttpError(
-                e.status().map(|status_code| status_code.as_u16()),
-                e.to_string(),
-            )
-        })?;
-
-        let response = response.text().await.map_err(|e| {
-            Error::HttpError(
-                e.status().map(|status_code| status_code.as_u16()),
-                e.to_string(),
-            )
-        })?;
-
-        serde_json::from_str::<R>(&response).map_err(|err| {
-            tracing::warn!("Http Response error: {}", err);
-            match ErrorResponse::from_json(&response) {
-                Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
-                Err(err) => err.into(),
-            }
-        })
-    }
-}
-
 type Cache = (u64, HashSet<(nut19::Method, nut19::Path)>);
 type Cache = (u64, HashSet<(nut19::Method, nut19::Path)>);
 
 
 /// Http Client
 /// Http Client
 #[derive(Debug, Clone)]
 #[derive(Debug, Clone)]
-pub struct HttpClient {
-    core: HttpClientCore,
+pub struct HttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
+    transport: Arc<T>,
     mint_url: MintUrl,
     mint_url: MintUrl,
     cache_support: Arc<StdRwLock<Cache>>,
     cache_support: Arc<StdRwLock<Cache>>,
     #[cfg(feature = "auth")]
     #[cfg(feature = "auth")]
     auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
     auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
 }
 }
 
 
-impl HttpClient {
+impl<T> HttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
     /// Create new [`HttpClient`]
     /// Create new [`HttpClient`]
     #[cfg(feature = "auth")]
     #[cfg(feature = "auth")]
     pub fn new(mint_url: MintUrl, auth_wallet: Option<AuthWallet>) -> Self {
     pub fn new(mint_url: MintUrl, auth_wallet: Option<AuthWallet>) -> Self {
         Self {
         Self {
-            core: HttpClientCore::new(),
+            transport: T::default().into(),
             mint_url,
             mint_url,
             auth_wallet: Arc::new(RwLock::new(auth_wallet)),
             auth_wallet: Arc::new(RwLock::new(auth_wallet)),
             cache_support: Default::default(),
             cache_support: Default::default(),
@@ -152,7 +62,7 @@ impl HttpClient {
     /// Create new [`HttpClient`]
     /// Create new [`HttpClient`]
     pub fn new(mint_url: MintUrl) -> Self {
     pub fn new(mint_url: MintUrl) -> Self {
         Self {
         Self {
-            core: HttpClientCore::new(),
+            transport: T::default().into(),
             cache_support: Default::default(),
             cache_support: Default::default(),
             mint_url,
             mint_url,
         }
         }
@@ -176,7 +86,6 @@ impl HttpClient {
         }
         }
     }
     }
 
 
-    #[cfg(not(target_arch = "wasm32"))]
     /// Create new [`HttpClient`] with a proxy for specific TLDs.
     /// Create new [`HttpClient`] with a proxy for specific TLDs.
     /// Specifying `None` for `host_matcher` will use the proxy for all
     /// Specifying `None` for `host_matcher` will use the proxy for all
     /// requests.
     /// requests.
@@ -186,32 +95,11 @@ impl HttpClient {
         host_matcher: Option<&str>,
         host_matcher: Option<&str>,
         accept_invalid_certs: bool,
         accept_invalid_certs: bool,
     ) -> Result<Self, Error> {
     ) -> Result<Self, Error> {
-        let regex = host_matcher
-            .map(regex::Regex::new)
-            .transpose()
-            .map_err(|e| Error::Custom(e.to_string()))?;
-        let client = reqwest::Client::builder()
-            .proxy(reqwest::Proxy::custom(move |url| {
-                if let Some(matcher) = regex.as_ref() {
-                    if let Some(host) = url.host_str() {
-                        if matcher.is_match(host) {
-                            return Some(proxy.clone());
-                        }
-                    }
-                }
-                None
-            }))
-            .danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs
-            .build()
-            .map_err(|e| {
-                Error::HttpError(
-                    e.status().map(|status_code| status_code.as_u16()),
-                    e.to_string(),
-                )
-            })?;
+        let mut transport = T::default();
+        transport.with_proxy(proxy, host_matcher, accept_invalid_certs)?;
 
 
         Ok(Self {
         Ok(Self {
-            core: HttpClientCore { inner: client },
+            transport: transport.into(),
             mint_url,
             mint_url,
             #[cfg(feature = "auth")]
             #[cfg(feature = "auth")]
             auth_wallet: Arc::new(RwLock::new(None)),
             auth_wallet: Arc::new(RwLock::new(None)),
@@ -231,7 +119,7 @@ impl HttpClient {
         payload: &P,
         payload: &P,
     ) -> Result<R, Error>
     ) -> Result<R, Error>
     where
     where
-        P: Serialize + ?Sized,
+        P: Serialize + ?Sized + Send + Sync,
         R: DeserializeOwned,
         R: DeserializeOwned,
     {
     {
         let started = Instant::now();
         let started = Instant::now();
@@ -259,8 +147,12 @@ impl HttpClient {
             })?;
             })?;
 
 
             let result = match method {
             let result = match method {
-                nut19::Method::Get => self.core.http_get(url, auth_token.clone()).await,
-                nut19::Method::Post => self.core.http_post(url, auth_token.clone(), payload).await,
+                nut19::Method::Get => self.transport.http_get(url, auth_token.clone()).await,
+                nut19::Method::Post => {
+                    self.transport
+                        .http_post(url, auth_token.clone(), payload)
+                        .await
+                }
             };
             };
 
 
             if result.is_ok() {
             if result.is_ok() {
@@ -291,15 +183,18 @@ impl HttpClient {
 
 
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
-impl MintConnector for HttpClient {
+impl<T> MintConnector for HttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
     /// Get Active Mint Keys [NUT-01]
     /// Get Active Mint Keys [NUT-01]
     #[instrument(skip(self), fields(mint_url = %self.mint_url))]
     #[instrument(skip(self), fields(mint_url = %self.mint_url))]
     async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
     async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
         let url = self.mint_url.join_paths(&["v1", "keys"])?;
         let url = self.mint_url.join_paths(&["v1", "keys"])?;
 
 
         Ok(self
         Ok(self
-            .core
-            .http_get::<_, KeysResponse>(url, None)
+            .transport
+            .http_get::<KeysResponse>(url, None)
             .await?
             .await?
             .keysets)
             .keysets)
     }
     }
@@ -311,7 +206,7 @@ impl MintConnector for HttpClient {
             .mint_url
             .mint_url
             .join_paths(&["v1", "keys", &keyset_id.to_string()])?;
             .join_paths(&["v1", "keys", &keyset_id.to_string()])?;
 
 
-        let keys_response = self.core.http_get::<_, KeysResponse>(url, None).await?;
+        let keys_response = self.transport.http_get::<KeysResponse>(url, None).await?;
 
 
         Ok(keys_response.keysets.first().unwrap().clone())
         Ok(keys_response.keysets.first().unwrap().clone())
     }
     }
@@ -320,7 +215,7 @@ impl MintConnector for HttpClient {
     #[instrument(skip(self), fields(mint_url = %self.mint_url))]
     #[instrument(skip(self), fields(mint_url = %self.mint_url))]
     async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
     async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
         let url = self.mint_url.join_paths(&["v1", "keysets"])?;
         let url = self.mint_url.join_paths(&["v1", "keysets"])?;
-        self.core.http_get(url, None).await
+        self.transport.http_get(url, None).await
     }
     }
 
 
     /// Mint Quote [NUT-04]
     /// Mint Quote [NUT-04]
@@ -341,7 +236,7 @@ impl MintConnector for HttpClient {
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
 
 
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
     }
 
 
     /// Mint Quote status
     /// Mint Quote status
@@ -361,7 +256,7 @@ impl MintConnector for HttpClient {
 
 
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
-        self.core.http_get(url, auth_token).await
+        self.transport.http_get(url, auth_token).await
     }
     }
 
 
     /// Mint Tokens [NUT-04]
     /// Mint Tokens [NUT-04]
@@ -399,7 +294,7 @@ impl MintConnector for HttpClient {
 
 
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
     }
 
 
     /// Melt Quote Status
     /// Melt Quote Status
@@ -419,7 +314,7 @@ impl MintConnector for HttpClient {
 
 
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
-        self.core.http_get(url, auth_token).await
+        self.transport.http_get(url, auth_token).await
     }
     }
 
 
     /// Melt [NUT-05]
     /// Melt [NUT-05]
@@ -467,7 +362,7 @@ impl MintConnector for HttpClient {
     /// Helper to get mint info
     /// Helper to get mint info
     async fn get_mint_info(&self) -> Result<MintInfo, Error> {
     async fn get_mint_info(&self) -> Result<MintInfo, Error> {
         let url = self.mint_url.join_paths(&["v1", "info"])?;
         let url = self.mint_url.join_paths(&["v1", "info"])?;
-        let info: MintInfo = self.core.http_get(url, None).await?;
+        let info: MintInfo = self.transport.http_get(url, None).await?;
 
 
         if let Ok(mut cache_support) = self.cache_support.write() {
         if let Ok(mut cache_support) = self.cache_support.write() {
             *cache_support = (
             *cache_support = (
@@ -509,7 +404,7 @@ impl MintConnector for HttpClient {
 
 
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
     }
 
 
     /// Restore request [NUT-13]
     /// Restore request [NUT-13]
@@ -523,7 +418,7 @@ impl MintConnector for HttpClient {
 
 
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
     }
 
 
     /// Mint Quote Bolt12 [NUT-23]
     /// Mint Quote Bolt12 [NUT-23]
@@ -544,7 +439,7 @@ impl MintConnector for HttpClient {
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
 
 
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
     }
 
 
     /// Mint Quote Bolt12 status
     /// Mint Quote Bolt12 status
@@ -564,7 +459,7 @@ impl MintConnector for HttpClient {
 
 
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
-        self.core.http_get(url, auth_token).await
+        self.transport.http_get(url, auth_token).await
     }
     }
 
 
     /// Melt Quote Bolt12 [NUT-23]
     /// Melt Quote Bolt12 [NUT-23]
@@ -583,7 +478,7 @@ impl MintConnector for HttpClient {
 
 
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
-        self.core.http_post(url, auth_token, &request).await
+        self.transport.http_post(url, auth_token, &request).await
     }
     }
 
 
     /// Melt Quote Bolt12 Status [NUT-23]
     /// Melt Quote Bolt12 Status [NUT-23]
@@ -603,7 +498,7 @@ impl MintConnector for HttpClient {
 
 
         #[cfg(not(feature = "auth"))]
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         let auth_token = None;
-        self.core.http_get(url, auth_token).await
+        self.transport.http_get(url, auth_token).await
     }
     }
 
 
     /// Melt Bolt12 [NUT-23]
     /// Melt Bolt12 [NUT-23]
@@ -632,18 +527,24 @@ impl MintConnector for HttpClient {
 /// Http Client
 /// Http Client
 #[derive(Debug, Clone)]
 #[derive(Debug, Clone)]
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
-pub struct AuthHttpClient {
-    core: HttpClientCore,
+pub struct AuthHttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
+    transport: Arc<T>,
     mint_url: MintUrl,
     mint_url: MintUrl,
     cat: Arc<RwLock<AuthToken>>,
     cat: Arc<RwLock<AuthToken>>,
 }
 }
 
 
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
-impl AuthHttpClient {
+impl<T> AuthHttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
     /// Create new [`AuthHttpClient`]
     /// Create new [`AuthHttpClient`]
     pub fn new(mint_url: MintUrl, cat: Option<AuthToken>) -> Self {
     pub fn new(mint_url: MintUrl, cat: Option<AuthToken>) -> Self {
         Self {
         Self {
-            core: HttpClientCore::new(),
+            transport: T::default().into(),
             mint_url,
             mint_url,
             cat: Arc::new(RwLock::new(
             cat: Arc::new(RwLock::new(
                 cat.unwrap_or(AuthToken::ClearAuth("".to_string())),
                 cat.unwrap_or(AuthToken::ClearAuth("".to_string())),
@@ -655,7 +556,10 @@ impl AuthHttpClient {
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
-impl AuthMintConnector for AuthHttpClient {
+impl<T> AuthMintConnector for AuthHttpClient<T>
+where
+    T: Transport + Send + Sync + 'static,
+{
     async fn get_auth_token(&self) -> Result<AuthToken, Error> {
     async fn get_auth_token(&self) -> Result<AuthToken, Error> {
         Ok(self.cat.read().await.clone())
         Ok(self.cat.read().await.clone())
     }
     }
@@ -668,7 +572,7 @@ impl AuthMintConnector for AuthHttpClient {
     /// Get Mint Info [NUT-06]
     /// Get Mint Info [NUT-06]
     async fn get_mint_info(&self) -> Result<MintInfo, Error> {
     async fn get_mint_info(&self) -> Result<MintInfo, Error> {
         let url = self.mint_url.join_paths(&["v1", "info"])?;
         let url = self.mint_url.join_paths(&["v1", "info"])?;
-        let mint_info: MintInfo = self.core.http_get::<_, MintInfo>(url, None).await?;
+        let mint_info: MintInfo = self.transport.http_get::<MintInfo>(url, None).await?;
 
 
         Ok(mint_info)
         Ok(mint_info)
     }
     }
@@ -680,7 +584,7 @@ impl AuthMintConnector for AuthHttpClient {
             self.mint_url
             self.mint_url
                 .join_paths(&["v1", "auth", "blind", "keys", &keyset_id.to_string()])?;
                 .join_paths(&["v1", "auth", "blind", "keys", &keyset_id.to_string()])?;
 
 
-        let mut keys_response = self.core.http_get::<_, KeysResponse>(url, None).await?;
+        let mut keys_response = self.transport.http_get::<KeysResponse>(url, None).await?;
 
 
         let keyset = keys_response
         let keyset = keys_response
             .keysets
             .keysets
@@ -698,14 +602,14 @@ impl AuthMintConnector for AuthHttpClient {
             .mint_url
             .mint_url
             .join_paths(&["v1", "auth", "blind", "keysets"])?;
             .join_paths(&["v1", "auth", "blind", "keysets"])?;
 
 
-        self.core.http_get(url, None).await
+        self.transport.http_get(url, None).await
     }
     }
 
 
     /// Mint Tokens [NUT-22]
     /// Mint Tokens [NUT-22]
     #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
     #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
     async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result<MintResponse, Error> {
     async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result<MintResponse, Error> {
         let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?;
         let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?;
-        self.core
+        self.transport
             .http_post(url, Some(self.cat.read().await.clone()), &request)
             .http_post(url, Some(self.cat.read().await.clone()), &request)
             .await
             .await
     }
     }

+ 6 - 3
crates/cdk/src/wallet/mint_connector/mod.rs

@@ -15,11 +15,14 @@ use crate::nuts::{
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
 use crate::wallet::AuthWallet;
 use crate::wallet::AuthWallet;
 
 
-mod http_client;
+pub mod http_client;
+pub mod transport;
 
 
+/// Auth HTTP Client with async transport
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
-pub use http_client::AuthHttpClient;
-pub use http_client::HttpClient;
+pub type AuthHttpClient = http_client::AuthHttpClient<transport::Async>;
+/// Http Client with async transport
+pub type HttpClient = http_client::HttpClient<transport::Async>;
 
 
 /// Interface that connects a wallet to a mint. Typically represents an [HttpClient].
 /// Interface that connects a wallet to a mint. Typically represents an [HttpClient].
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]

+ 182 - 0
crates/cdk/src/wallet/mint_connector/transport.rs

@@ -0,0 +1,182 @@
+//! HTTP Transport trait with a default implementation
+use std::fmt::Debug;
+
+use cdk_common::AuthToken;
+use reqwest::Client;
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use url::Url;
+
+use super::Error;
+use crate::error::ErrorResponse;
+
+/// Expected HTTP Transport
+#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
+pub trait Transport: Default + Send + Sync + Debug + Clone {
+    /// Make the transport to use a given proxy
+    fn with_proxy(
+        &mut self,
+        proxy: Url,
+        host_matcher: Option<&str>,
+        accept_invalid_certs: bool,
+    ) -> Result<(), Error>;
+
+    /// HTTP Get request
+    async fn http_get<R>(&self, url: Url, auth: Option<AuthToken>) -> Result<R, Error>
+    where
+        R: DeserializeOwned;
+
+    /// HTTP Post request
+    async fn http_post<P, R>(
+        &self,
+        url: Url,
+        auth_token: Option<AuthToken>,
+        payload: &P,
+    ) -> Result<R, Error>
+    where
+        P: Serialize + ?Sized + Send + Sync,
+        R: DeserializeOwned;
+}
+
+/// Async transport for Http
+#[derive(Debug, Clone)]
+pub struct Async {
+    inner: Client,
+}
+
+impl Default for Async {
+    fn default() -> Self {
+        #[cfg(not(target_arch = "wasm32"))]
+        if rustls::crypto::CryptoProvider::get_default().is_none() {
+            let _ = rustls::crypto::ring::default_provider().install_default();
+        }
+
+        Self {
+            inner: Client::new(),
+        }
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
+impl Transport for Async {
+    #[cfg(target_arch = "wasm32")]
+    fn with_proxy(
+        &mut self,
+        _proxy: Url,
+        _host_matcher: Option<&str>,
+        _accept_invalid_certs: bool,
+    ) -> Result<(), Error> {
+        panic!("Not supported in wasm");
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    fn with_proxy(
+        &mut self,
+        proxy: Url,
+        host_matcher: Option<&str>,
+        accept_invalid_certs: bool,
+    ) -> Result<(), Error> {
+        let regex = host_matcher
+            .map(regex::Regex::new)
+            .transpose()
+            .map_err(|e| Error::Custom(e.to_string()))?;
+        self.inner = reqwest::Client::builder()
+            .proxy(reqwest::Proxy::custom(move |url| {
+                if let Some(matcher) = regex.as_ref() {
+                    if let Some(host) = url.host_str() {
+                        if matcher.is_match(host) {
+                            return Some(proxy.clone());
+                        }
+                    }
+                }
+                None
+            }))
+            .danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs
+            .build()
+            .map_err(|e| {
+                Error::HttpError(
+                    e.status().map(|status_code| status_code.as_u16()),
+                    e.to_string(),
+                )
+            })?;
+        Ok(())
+    }
+
+    async fn http_get<R>(&self, url: Url, auth: Option<AuthToken>) -> Result<R, Error>
+    where
+        R: DeserializeOwned,
+    {
+        let mut request = self.inner.get(url);
+
+        if let Some(auth) = auth {
+            request = request.header(auth.header_key(), auth.to_string());
+        }
+
+        let response = request
+            .send()
+            .await
+            .map_err(|e| {
+                Error::HttpError(
+                    e.status().map(|status_code| status_code.as_u16()),
+                    e.to_string(),
+                )
+            })?
+            .text()
+            .await
+            .map_err(|e| {
+                Error::HttpError(
+                    e.status().map(|status_code| status_code.as_u16()),
+                    e.to_string(),
+                )
+            })?;
+
+        serde_json::from_str::<R>(&response).map_err(|err| {
+            tracing::warn!("Http Response error: {}", err);
+            match ErrorResponse::from_json(&response) {
+                Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
+                Err(err) => err.into(),
+            }
+        })
+    }
+
+    async fn http_post<P, R>(
+        &self,
+        url: Url,
+        auth_token: Option<AuthToken>,
+        payload: &P,
+    ) -> Result<R, Error>
+    where
+        P: Serialize + ?Sized + Send + Sync,
+        R: DeserializeOwned,
+    {
+        let mut request = self.inner.post(url).json(&payload);
+
+        if let Some(auth) = auth_token {
+            request = request.header(auth.header_key(), auth.to_string());
+        }
+
+        let response = request.send().await.map_err(|e| {
+            Error::HttpError(
+                e.status().map(|status_code| status_code.as_u16()),
+                e.to_string(),
+            )
+        })?;
+
+        let response = response.text().await.map_err(|e| {
+            Error::HttpError(
+                e.status().map(|status_code| status_code.as_u16()),
+                e.to_string(),
+            )
+        })?;
+
+        serde_json::from_str::<R>(&response).map_err(|err| {
+            tracing::warn!("Http Response error: {}", err);
+            match ErrorResponse::from_json(&response) {
+                Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
+                Err(err) => err.into(),
+            }
+        })
+    }
+}

+ 4 - 0
crates/cdk/src/wallet/mod.rs

@@ -54,6 +54,10 @@ pub use auth::{AuthMintConnector, AuthWallet};
 pub use builder::WalletBuilder;
 pub use builder::WalletBuilder;
 pub use cdk_common::wallet as types;
 pub use cdk_common::wallet as types;
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
+pub use mint_connector::http_client::AuthHttpClient as BaseAuthHttpClient;
+pub use mint_connector::http_client::HttpClient as BaseHttpClient;
+pub use mint_connector::transport::Transport as HttpTransport;
+#[cfg(feature = "auth")]
 pub use mint_connector::AuthHttpClient;
 pub use mint_connector::AuthHttpClient;
 pub use mint_connector::{HttpClient, MintConnector};
 pub use mint_connector::{HttpClient, MintConnector};
 pub use multi_mint_wallet::MultiMintWallet;
 pub use multi_mint_wallet::MultiMintWallet;

+ 21 - 1
crates/cdk/src/wallet/subscription/http.rs

@@ -2,6 +2,7 @@ use std::collections::HashMap;
 use std::sync::Arc;
 use std::sync::Arc;
 use std::time::Duration;
 use std::time::Duration;
 
 
+use cdk_common::MintQuoteBolt12Response;
 use tokio::sync::{mpsc, RwLock};
 use tokio::sync::{mpsc, RwLock};
 use tokio::time;
 use tokio::time;
 
 
@@ -15,6 +16,7 @@ use crate::Wallet;
 #[derive(Debug, Hash, PartialEq, Eq)]
 #[derive(Debug, Hash, PartialEq, Eq)]
 enum UrlType {
 enum UrlType {
     Mint(String),
     Mint(String),
+    MintBolt12(String),
     Melt(String),
     Melt(String),
     PublicKey(nut01::PublicKey),
     PublicKey(nut01::PublicKey),
 }
 }
@@ -22,6 +24,7 @@ enum UrlType {
 #[derive(Debug, Eq, PartialEq)]
 #[derive(Debug, Eq, PartialEq)]
 enum AnyState {
 enum AnyState {
     MintQuoteState(nut23::QuoteState),
     MintQuoteState(nut23::QuoteState),
+    MintBolt12QuoteState(MintQuoteBolt12Response<String>),
     MeltQuoteState(nut05::QuoteState),
     MeltQuoteState(nut05::QuoteState),
     PublicKey(nut07::State),
     PublicKey(nut07::State),
     Empty,
     Empty,
@@ -67,7 +70,12 @@ async fn convert_subscription(
             }
             }
         }
         }
         Kind::Bolt12MintQuote => {
         Kind::Bolt12MintQuote => {
-            for id in sub.1.filters.iter().map(|id| UrlType::Mint(id.clone())) {
+            for id in sub
+                .1
+                .filters
+                .iter()
+                .map(|id| UrlType::MintBolt12(id.clone()))
+            {
                 subscribed_to.insert(id, (sub.0.clone(), sub.1.id.clone(), AnyState::Empty));
                 subscribed_to.insert(id, (sub.0.clone(), sub.1.id.clone(), AnyState::Empty));
             }
             }
         }
         }
@@ -98,6 +106,18 @@ pub async fn http_main<S: IntoIterator<Item = SubId>>(
                 for (url, (sender, _, last_state)) in subscribed_to.iter_mut() {
                 for (url, (sender, _, last_state)) in subscribed_to.iter_mut() {
                     tracing::debug!("Polling: {:?}", url);
                     tracing::debug!("Polling: {:?}", url);
                     match url {
                     match url {
+                        UrlType::MintBolt12(id) => {
+                            let response = http_client.get_mint_quote_bolt12_status(id).await;
+                            if let Ok(response) = response {
+                                if *last_state == AnyState::MintBolt12QuoteState(response.clone()) {
+                                    continue;
+                                }
+                                *last_state = AnyState::MintBolt12QuoteState(response.clone());
+                                if let Err(err) = sender.try_send(NotificationPayload::MintQuoteBolt12Response(response)) {
+                                    tracing::error!("Error sending mint quote response: {:?}", err);
+                                }
+                            }
+                        },
                         UrlType::Mint(id) => {
                         UrlType::Mint(id) => {
 
 
                             let response = http_client.get_mint_quote_status(id).await;
                             let response = http_client.get_mint_quote_status(id).await;

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

@@ -48,14 +48,16 @@ type WsSubscriptionBody = (mpsc::Sender<NotificationPayload>, Params);
 pub struct SubscriptionManager {
 pub struct SubscriptionManager {
     all_connections: Arc<RwLock<HashMap<MintUrl, SubscriptionClient>>>,
     all_connections: Arc<RwLock<HashMap<MintUrl, SubscriptionClient>>>,
     http_client: Arc<dyn MintConnector + Send + Sync>,
     http_client: Arc<dyn MintConnector + Send + Sync>,
+    prefer_http: bool,
 }
 }
 
 
 impl SubscriptionManager {
 impl SubscriptionManager {
     /// Create a new subscription manager
     /// Create a new subscription manager
-    pub fn new(http_client: Arc<dyn MintConnector + Send + Sync>) -> Self {
+    pub fn new(http_client: Arc<dyn MintConnector + Send + Sync>, prefer_http: bool) -> Self {
         Self {
         Self {
             all_connections: Arc::new(RwLock::new(HashMap::new())),
             all_connections: Arc::new(RwLock::new(HashMap::new())),
             http_client,
             http_client,
+            prefer_http,
         }
         }
     }
     }
 
 
@@ -93,6 +95,12 @@ impl SubscriptionManager {
             ))]
             ))]
             let is_ws_support = false;
             let is_ws_support = false;
 
 
+            let is_ws_support = if self.prefer_http {
+                false
+            } else {
+                is_ws_support
+            };
+
             tracing::debug!(
             tracing::debug!(
                 "Connect to {:?} to subscribe. WebSocket is supported ({})",
                 "Connect to {:?} to subscribe. WebSocket is supported ({})",
                 mint_url,
                 mint_url,

+ 10 - 26
crates/cdk/src/wallet/subscription/ws.rs

@@ -18,25 +18,6 @@ use crate::Wallet;
 
 
 const MAX_ATTEMPT_FALLBACK_HTTP: usize = 10;
 const MAX_ATTEMPT_FALLBACK_HTTP: usize = 10;
 
 
-async fn fallback_to_http<S: IntoIterator<Item = SubId>>(
-    initial_state: S,
-    http_client: Arc<dyn MintConnector + Send + Sync>,
-    subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
-    new_subscription_recv: mpsc::Receiver<SubId>,
-    on_drop: mpsc::Receiver<SubId>,
-    wallet: Arc<Wallet>,
-) {
-    http_main(
-        initial_state,
-        http_client,
-        subscriptions,
-        new_subscription_recv,
-        on_drop,
-        wallet,
-    )
-    .await
-}
-
 #[inline]
 #[inline]
 pub async fn ws_main(
 pub async fn ws_main(
     http_client: Arc<dyn MintConnector + Send + Sync>,
     http_client: Arc<dyn MintConnector + Send + Sync>,
@@ -72,7 +53,8 @@ pub async fn ws_main(
                     tracing::error!(
                     tracing::error!(
                         "Could not connect to server after {MAX_ATTEMPT_FALLBACK_HTTP} attempts, falling back to HTTP-subscription client"
                         "Could not connect to server after {MAX_ATTEMPT_FALLBACK_HTTP} attempts, falling back to HTTP-subscription client"
                     );
                     );
-                    return fallback_to_http(
+
+                    return http_main(
                         active_subscriptions.into_keys(),
                         active_subscriptions.into_keys(),
                         http_client,
                         http_client,
                         subscriptions,
                         subscriptions,
@@ -169,17 +151,19 @@ pub async fn ws_main(
                         WsMessageOrResponse::ErrorResponse(error) => {
                         WsMessageOrResponse::ErrorResponse(error) => {
                             tracing::error!("Received error from server: {:?}", error);
                             tracing::error!("Received error from server: {:?}", error);
                             if subscription_requests.contains(&error.id) {
                             if subscription_requests.contains(&error.id) {
-                                // If the server sends an error response to a subscription request, we should
-                                // fallback to HTTP.
-                                // TODO: Add some retry before giving up to HTTP.
-                                return fallback_to_http(
+                                tracing::error!(
+                                    "Falling back to HTTP client"
+                                );
+
+                                return http_main(
                                     active_subscriptions.into_keys(),
                                     active_subscriptions.into_keys(),
                                     http_client,
                                     http_client,
                                     subscriptions,
                                     subscriptions,
                                     new_subscription_recv,
                                     new_subscription_recv,
                                     on_drop,
                                     on_drop,
-                                    wallet
-                                ).await;
+                                    wallet,
+                                )
+                                .await;
                             }
                             }
                         }
                         }
                     }
                     }

+ 9 - 9
flake.lock

@@ -23,11 +23,11 @@
         "rust-analyzer-src": []
         "rust-analyzer-src": []
       },
       },
       "locked": {
       "locked": {
-        "lastModified": 1755585599,
-        "narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=",
+        "lastModified": 1756622179,
+        "narHash": "sha256-K3CimrAcMhdDYkErd3oiWPZNaoyaGZEuvGrFuDPFMZY=",
         "owner": "nix-community",
         "owner": "nix-community",
         "repo": "fenix",
         "repo": "fenix",
-        "rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42",
+        "rev": "0abcb15ae6279dcb40a8ae7c1ed980705245cb79",
         "type": "github"
         "type": "github"
       },
       },
       "original": {
       "original": {
@@ -93,11 +93,11 @@
     },
     },
     "nixpkgs": {
     "nixpkgs": {
       "locked": {
       "locked": {
-        "lastModified": 1755922037,
-        "narHash": "sha256-wY1+2JPH0ZZC4BQefoZw/k+3+DowFyfOxv17CN/idKs=",
+        "lastModified": 1756469547,
+        "narHash": "sha256-YvtD2E7MYsQ3r7K9K2G7nCslCKMPShoSEAtbjHLtH0k=",
         "owner": "NixOS",
         "owner": "NixOS",
         "repo": "nixpkgs",
         "repo": "nixpkgs",
-        "rev": "b1b3291469652d5a2edb0becc4ef0246fff97a7c",
+        "rev": "41d292bfc37309790f70f4c120b79280ce40af16",
         "type": "github"
         "type": "github"
       },
       },
       "original": {
       "original": {
@@ -160,11 +160,11 @@
         ]
         ]
       },
       },
       "locked": {
       "locked": {
-        "lastModified": 1756089517,
-        "narHash": "sha256-KGinVKturJFPrRebgvyUB1BUNqf1y9FN+tSJaTPlnFE=",
+        "lastModified": 1756607787,
+        "narHash": "sha256-ciwAdgtlAN1PCaidWK6RuWsTBL8DVuyDCGM+X3ein5Q=",
         "owner": "oxalica",
         "owner": "oxalica",
         "repo": "rust-overlay",
         "repo": "rust-overlay",
-        "rev": "44774c8c83cd392c50914f86e1ff75ef8619f1cd",
+        "rev": "f46d294b87ebb9f7124f1ce13aa2a5f5acc0f3eb",
         "type": "github"
         "type": "github"
       },
       },
       "original": {
       "original": {

+ 5 - 1
flake.nix

@@ -68,6 +68,11 @@
         # MSRV stable
         # MSRV stable
         msrv_toolchain = pkgs.rust-bin.stable."1.85.0".default.override {
         msrv_toolchain = pkgs.rust-bin.stable."1.85.0".default.override {
           targets = [ "wasm32-unknown-unknown" ]; # wasm
           targets = [ "wasm32-unknown-unknown" ]; # wasm
+          extensions = [
+            "rustfmt"
+            "clippy"
+            "rust-analyzer"
+          ];
         };
         };
 
 
         # Nightly used for formatting
         # Nightly used for formatting
@@ -114,7 +119,6 @@
 
 
         # Common arguments can be set here to avoid repeating them later
         # Common arguments can be set here to avoid repeating them later
         nativeBuildInputs = [
         nativeBuildInputs = [
-          pkgs.rust-analyzer
           #Add additional build inputs here
           #Add additional build inputs here
         ]
         ]
         ++ lib.optionals isDarwin [
         ++ lib.optionals isDarwin [

+ 2 - 0
justfile

@@ -325,6 +325,7 @@ release m="":
     "-p cdk-common"
     "-p cdk-common"
     "-p cdk-sql-common"
     "-p cdk-sql-common"
     "-p cdk-sqlite"
     "-p cdk-sqlite"
+    "-p cdk-postgres"
     "-p cdk-redb"
     "-p cdk-redb"
     "-p cdk-signatory"
     "-p cdk-signatory"
     "-p cdk"
     "-p cdk"
@@ -333,6 +334,7 @@ release m="":
     "-p cdk-cln"
     "-p cdk-cln"
     "-p cdk-lnd"
     "-p cdk-lnd"
     "-p cdk-lnbits"
     "-p cdk-lnbits"
+    "-p cdk-ldk-node"
     "-p cdk-fake-wallet"
     "-p cdk-fake-wallet"
     "-p cdk-payment-processor"
     "-p cdk-payment-processor"
     "-p cdk-cli"
     "-p cdk-cli"

Some files were not shown because too many files changed in this diff