Kaynağa Gözat

feat: melt bolt12 ws sub (#1598)

tsk 1 ay önce
ebeveyn
işleme
085f7778ce

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

@@ -75,7 +75,10 @@ pub use nut23::{
     MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
     MintQuoteBolt11Response, QuoteState as MintQuoteState,
 };
-pub use nut25::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
+pub use nut25::{
+    MeltQuoteBolt12Request, MeltQuoteBolt12Response, MintQuoteBolt12Request,
+    MintQuoteBolt12Response,
+};
 #[cfg(all(feature = "wallet", feature = "nostr"))]
 pub use nut27::{
     backup_filter_params, create_backup_event, decrypt_backup_event, derive_nostr_keys, MintBackup,

+ 61 - 3
crates/cashu/src/nuts/nut17/mod.rs

@@ -4,8 +4,10 @@ use serde::{Deserialize, Serialize};
 
 use super::PublicKey;
 use crate::nut00::KnownMethod;
+use crate::nut25::MeltQuoteBolt12Response;
 use crate::nuts::{
-    CurrencyUnit, MeltQuoteBolt11Response, MintQuoteBolt11Response, PaymentMethod, ProofState,
+    CurrencyUnit, MeltQuoteBolt11Response, MeltQuoteCustomResponse, MintQuoteBolt11Response,
+    MintQuoteCustomResponse, PaymentMethod, ProofState,
 };
 use crate::quote_id::QuoteIdError;
 use crate::MintQuoteBolt12Response;
@@ -175,6 +177,15 @@ where
     }
 }
 
+impl<T> From<MeltQuoteBolt12Response<T>> for NotificationPayload<T>
+where
+    T: Clone,
+{
+    fn from(melt_quote: MeltQuoteBolt12Response<T>) -> NotificationPayload<T> {
+        NotificationPayload::MeltQuoteBolt12Response(melt_quote)
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(bound = "T: Serialize + DeserializeOwned")]
 #[serde(untagged)]
@@ -191,6 +202,12 @@ where
     MintQuoteBolt11Response(MintQuoteBolt11Response<T>),
     /// Mint Quote Bolt12 Response
     MintQuoteBolt12Response(MintQuoteBolt12Response<T>),
+    /// Melt Quote Bolt12 Response
+    MeltQuoteBolt12Response(MeltQuoteBolt12Response<T>),
+    /// Custom Mint Quote Response (method, response)
+    CustomMintQuoteResponse(String, MintQuoteCustomResponse<T>),
+    /// Custom Melt Quote Response (method, response)
+    CustomMeltQuoteResponse(String, MeltQuoteCustomResponse<T>),
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Hash, Serialize)]
@@ -210,11 +227,14 @@ where
     MintQuoteBolt12(T),
     /// MintQuote id is an QuoteId
     MeltQuoteBolt12(T),
+    /// MintQuote id is an QuoteId
+    MintQuoteCustom(String, T),
+    /// MintQuote id is an QuoteId
+    MeltQuoteCustom(String, T),
 }
 
 /// Kind
-#[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
+#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Hash)]
 pub enum Kind {
     /// Bolt 11 Melt Quote
     Bolt11MeltQuote,
@@ -224,6 +244,44 @@ pub enum Kind {
     ProofState,
     /// Bolt 12 Mint Quote
     Bolt12MintQuote,
+    /// Bolt 12 Melt Quote
+    Bolt12MeltQuote,
+    /// Custom
+    Custom(String),
+}
+
+impl Serialize for Kind {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let s = match self {
+            Kind::Bolt11MintQuote => "bolt11_mint_quote",
+            Kind::Bolt11MeltQuote => "bolt11_melt_quote",
+            Kind::Bolt12MintQuote => "bolt12_mint_quote",
+            Kind::Bolt12MeltQuote => "bolt12_melt_quote",
+            Kind::ProofState => "proof_state",
+            Kind::Custom(custom) => custom.as_str(),
+        };
+        serializer.serialize_str(s)
+    }
+}
+
+impl<'de> Deserialize<'de> for Kind {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        Ok(match s.as_str() {
+            "bolt11_mint_quote" => Kind::Bolt11MintQuote,
+            "bolt11_melt_quote" => Kind::Bolt11MeltQuote,
+            "bolt12_mint_quote" => Kind::Bolt12MintQuote,
+            "bolt12_melt_quote" => Kind::Bolt12MeltQuote,
+            "proof_state" => Kind::ProofState,
+            custom => Kind::Custom(custom.to_string()),
+        })
+    }
 }
 
 impl<I> AsRef<I> for Params<I> {

+ 1 - 1
crates/cashu/src/nuts/nut17/ws.rs

@@ -167,7 +167,7 @@ pub enum WsMessageOrResponse<I> {
     /// An error response
     ErrorResponse(WsErrorResponse),
     /// A notification
-    Notification(WsNotification<NotificationInner<String, I>>),
+    Notification(Box<WsNotification<NotificationInner<String, I>>>),
 }
 
 impl<I> From<(usize, Result<WsResponseResult<I>, WsErrorBody>)> for WsMessageOrResponse<I> {

+ 3 - 0
crates/cashu/src/nuts/nut25.rs

@@ -102,3 +102,6 @@ pub struct MeltQuoteBolt12Request {
     /// Payment Options
     pub options: Option<MeltOptions>,
 }
+
+/// Melt quote response [NUT-25]
+pub type MeltQuoteBolt12Response<Q> = crate::nuts::nut23::MeltQuoteBolt11Response<Q>;

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

@@ -41,6 +41,22 @@ impl SubscriptionRequest for Params {
                 Kind::Bolt12MintQuote => QuoteId::from_str(filter)
                     .map(NotificationId::MintQuoteBolt12)
                     .map_err(|_| Error::ParsingError(filter.to_owned())),
+                Kind::Bolt12MeltQuote => QuoteId::from_str(filter)
+                    .map(NotificationId::MeltQuoteBolt12)
+                    .map_err(|_| Error::ParsingError(filter.to_owned())),
+                Kind::Custom(ref s) => {
+                    if let Some(method) = s.strip_suffix("_mint_quote") {
+                        QuoteId::from_str(filter)
+                            .map(|id| NotificationId::MintQuoteCustom(method.to_string(), id))
+                            .map_err(|_| Error::ParsingError(filter.to_owned()))
+                    } else if let Some(method) = s.strip_suffix("_melt_quote") {
+                        QuoteId::from_str(filter)
+                            .map(|id| NotificationId::MeltQuoteCustom(method.to_string(), id))
+                            .map_err(|_| Error::ParsingError(filter.to_owned()))
+                    } else {
+                        Err(Error::ParsingError(filter.to_owned()))
+                    }
+                }
             })
             .collect::<Result<Vec<_>, _>>()
     }
@@ -73,6 +89,18 @@ impl SubscriptionRequest for WalletParams {
                         .map_err(|_| Error::ParsingError(filter.to_owned()))?,
 
                     Kind::Bolt12MintQuote => NotificationId::MintQuoteBolt12(filter.to_owned()),
+                    Kind::Bolt12MeltQuote => NotificationId::MeltQuoteBolt12(filter.to_owned()),
+                    Kind::Custom(ref s) => {
+                        if let Some(method) = s.strip_suffix("_mint_quote") {
+                            NotificationId::MintQuoteCustom(method.to_string(), filter.to_owned())
+                        } else if let Some(method) = s.strip_suffix("_melt_quote") {
+                            NotificationId::MeltQuoteCustom(method.to_string(), filter.to_owned())
+                        } else {
+                            // If we can't parse the custom method, we can't create a NotificationId
+                            // This might happen if the custom kind doesn't follow the convention
+                            return Err(Error::ParsingError(format!("Invalid custom kind: {}", s)));
+                        }
+                    }
                 })
             })
             .collect::<Result<Vec<_>, _>>()

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

@@ -65,6 +65,15 @@ pub fn notification_uuid_to_notification_string(
             NotificationPayload::MintQuoteBolt12Response(quote) => {
                 NotificationPayload::MintQuoteBolt12Response(quote.to_string_id())
             }
+            NotificationPayload::MeltQuoteBolt12Response(quote) => {
+                NotificationPayload::MeltQuoteBolt12Response(quote.to_string_id())
+            }
+            NotificationPayload::CustomMintQuoteResponse(method, quote) => {
+                NotificationPayload::CustomMintQuoteResponse(method, quote.to_string_id())
+            }
+            NotificationPayload::CustomMeltQuoteResponse(method, quote) => {
+                NotificationPayload::CustomMeltQuoteResponse(method, quote.to_string_id())
+            }
         },
     }
 }
@@ -72,9 +81,9 @@ pub fn notification_uuid_to_notification_string(
 #[cfg(feature = "mint")]
 /// Converts a notification to a websocket message that can be sent to clients
 pub fn notification_to_ws_message(notification: NotificationInner<QuoteId>) -> WsMessageOrResponse {
-    nut17::ws::WsMessageOrResponse::Notification(nut17::ws::WsNotification {
+    nut17::ws::WsMessageOrResponse::Notification(Box::new(nut17::ws::WsNotification {
         jsonrpc: JSON_RPC_VERSION.to_owned(),
         method: "subscribe".to_string(),
         params: notification_uuid_to_notification_string(notification),
-    })
+    }))
 }

+ 7 - 0
crates/cdk-ffi/src/types/subscription.rs

@@ -17,6 +17,8 @@ pub enum SubscriptionKind {
     Bolt11MintQuote,
     /// Bolt 12 Mint Quote
     Bolt12MintQuote,
+    /// Bolt 12 Melt Quote
+    Bolt12MeltQuote,
     /// Proof State
     ProofState,
 }
@@ -27,6 +29,7 @@ impl From<SubscriptionKind> for cdk::nuts::nut17::Kind {
             SubscriptionKind::Bolt11MeltQuote => cdk::nuts::nut17::Kind::Bolt11MeltQuote,
             SubscriptionKind::Bolt11MintQuote => cdk::nuts::nut17::Kind::Bolt11MintQuote,
             SubscriptionKind::Bolt12MintQuote => cdk::nuts::nut17::Kind::Bolt12MintQuote,
+            SubscriptionKind::Bolt12MeltQuote => cdk::nuts::nut17::Kind::Bolt12MeltQuote,
             SubscriptionKind::ProofState => cdk::nuts::nut17::Kind::ProofState,
         }
     }
@@ -38,6 +41,10 @@ impl From<cdk::nuts::nut17::Kind> for SubscriptionKind {
             cdk::nuts::nut17::Kind::Bolt11MeltQuote => SubscriptionKind::Bolt11MeltQuote,
             cdk::nuts::nut17::Kind::Bolt11MintQuote => SubscriptionKind::Bolt11MintQuote,
             cdk::nuts::nut17::Kind::Bolt12MintQuote => SubscriptionKind::Bolt12MintQuote,
+            cdk::nuts::nut17::Kind::Bolt12MeltQuote => SubscriptionKind::Bolt12MeltQuote,
+            cdk::nuts::nut17::Kind::Custom(_) => {
+                panic!("Custom subscription kind not supported in FFI")
+            }
             cdk::nuts::nut17::Kind::ProofState => SubscriptionKind::ProofState,
         }
     }

+ 9 - 0
crates/cdk/src/event.rs

@@ -121,6 +121,15 @@ where
             NotificationPayload::MintQuoteBolt12Response(r) => {
                 NotificationId::MintQuoteBolt12(r.quote.to_owned())
             }
+            NotificationPayload::MeltQuoteBolt12Response(r) => {
+                NotificationId::MeltQuoteBolt12(r.quote.to_owned())
+            }
+            NotificationPayload::CustomMintQuoteResponse(method, r) => {
+                NotificationId::MintQuoteCustom(method.clone(), r.quote.to_owned())
+            }
+            NotificationPayload::CustomMeltQuoteResponse(method, r) => {
+                NotificationId::MeltQuoteCustom(method.clone(), r.quote.to_owned())
+            }
             NotificationPayload::ProofState(p) => NotificationId::ProofState(p.y.to_owned()),
         }]
     }

+ 19 - 6
crates/cdk/src/mint/subscription.rs

@@ -13,8 +13,8 @@ use cdk_common::pub_sub::{Pubsub, Spec, Subscriber};
 use cdk_common::subscription::SubId;
 use cdk_common::{
     Amount, BlindSignature, CurrencyUnit, MeltQuoteBolt11Response, MeltQuoteState,
-    MintQuoteBolt11Response, MintQuoteBolt12Response, MintQuoteState, ProofState, PublicKey,
-    QuoteId,
+    MintQuoteBolt11Response, MintQuoteBolt12Response, MintQuoteCustomResponse, MintQuoteState,
+    NotificationPayload, ProofState, PublicKey, QuoteId,
 };
 
 use super::Mint;
@@ -96,6 +96,9 @@ impl MintPubSubSpec {
                         to_return.push(mint_quote);
                     }
                 }
+                NotificationId::MintQuoteCustom(_, _) | NotificationId::MeltQuoteCustom(_, _) => {
+                    continue;
+                }
             }
         }
 
@@ -181,8 +184,13 @@ impl PubSubManager {
                     total_issued.into(),
                 );
             }
-            _ => {
-                // We don't send ws updates for unknown methods
+            cdk_common::PaymentMethod::Custom(ref method) => {
+                if let Ok(response) = MintQuoteCustomResponse::try_from(mint_quote.clone()) {
+                    self.publish(NotificationPayload::CustomMintQuoteResponse(
+                        method.clone(),
+                        response,
+                    ));
+                }
             }
         }
     }
@@ -200,8 +208,13 @@ impl PubSubManager {
                     mint_quote.amount_issued().into(),
                 );
             }
-            _ => {
-                // We don't send ws updates for unknown methods
+            cdk_common::PaymentMethod::Custom(ref method) => {
+                if let Ok(response) = MintQuoteCustomResponse::try_from(mint_quote.clone()) {
+                    self.publish(NotificationPayload::CustomMintQuoteResponse(
+                        method.clone(),
+                        response,
+                    ));
+                }
             }
         }
     }

+ 10 - 6
crates/cdk/src/wallet/melt/mod.rs

@@ -66,8 +66,10 @@ use saga::MeltSaga;
 /// identical fields but different Rust types.
 #[derive(Debug, Clone)]
 pub(crate) enum MeltQuoteStatusResponse {
-    /// Standard response (Bolt11/Bolt12)
+    /// Standard response (Bolt11)
     Standard(cdk_common::MeltQuoteBolt11Response<String>),
+    /// Bolt12 response
+    Bolt12(cdk_common::MeltQuoteBolt12Response<String>),
     /// Custom payment method response
     Custom(cdk_common::MeltQuoteCustomResponse<String>),
 }
@@ -77,6 +79,7 @@ impl MeltQuoteStatusResponse {
     pub fn state(&self) -> MeltQuoteState {
         match self {
             Self::Standard(r) => r.state,
+            Self::Bolt12(r) => r.state,
             Self::Custom(r) => r.state,
         }
     }
@@ -85,17 +88,18 @@ impl MeltQuoteStatusResponse {
     pub fn payment_preimage(&self) -> Option<String> {
         match self {
             Self::Standard(r) => r.payment_preimage.clone(),
+            Self::Bolt12(r) => r.payment_preimage.clone(),
             Self::Custom(r) => r.payment_preimage.clone(),
         }
     }
 
-    /// Convert to standard response (for Bolt11/Bolt12).
-    /// Returns error for Custom payment methods.
+    /// Convert to standard response (for Bolt11).
+    /// Returns error for Custom payment methods and Bolt12 (since types differ).
     pub fn into_standard(self) -> Result<cdk_common::MeltQuoteBolt11Response<String>, Error> {
         match self {
             Self::Standard(r) => Ok(r),
-            Self::Custom(_) => Err(Error::Custom(
-                "Cannot convert custom response to standard response".to_string(),
+            _ => Err(Error::Custom(
+                "Cannot convert response to standard bolt11 response".to_string(),
             )),
         }
     }
@@ -798,7 +802,7 @@ impl Wallet {
             }
             PaymentMethod::Known(KnownMethod::Bolt12) => {
                 let r = self.client.get_melt_bolt12_quote_status(quote_id).await?;
-                MeltQuoteStatusResponse::Standard(r)
+                MeltQuoteStatusResponse::Bolt12(r)
             }
             PaymentMethod::Custom(method) => {
                 let r = self

+ 4 - 4
crates/cdk/src/wallet/mint_connector/http_client.rs

@@ -4,8 +4,8 @@ use std::sync::{Arc, RwLock as StdRwLock};
 
 use async_trait::async_trait;
 use cdk_common::{
-    nut19, MeltQuoteBolt12Request, MeltQuoteCustomResponse, MintQuoteBolt12Request,
-    MintQuoteBolt12Response,
+    nut19, MeltQuoteBolt12Request, MeltQuoteBolt12Response, MeltQuoteCustomResponse,
+    MintQuoteBolt12Request, MintQuoteBolt12Response,
 };
 #[cfg(feature = "auth")]
 use cdk_common::{Method, ProtectedEndpoint, RoutePath};
@@ -563,7 +563,7 @@ where
     async fn post_melt_bolt12_quote(
         &self,
         request: MeltQuoteBolt12Request,
-    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+    ) -> Result<MeltQuoteBolt12Response<String>, Error> {
         let url = self
             .mint_url
             .join_paths(&["v1", "melt", "quote", "bolt12"])?;
@@ -585,7 +585,7 @@ where
     async fn get_melt_bolt12_quote_status(
         &self,
         quote_id: &str,
-    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+    ) -> Result<MeltQuoteBolt12Response<String>, Error> {
         let url = self
             .mint_url
             .join_paths(&["v1", "melt", "quote", "bolt12", quote_id])?;

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

@@ -4,8 +4,8 @@ use std::fmt::Debug;
 
 use async_trait::async_trait;
 use cdk_common::{
-    MeltQuoteBolt12Request, MeltQuoteCustomResponse, MintQuoteBolt12Request,
-    MintQuoteBolt12Response,
+    MeltQuoteBolt12Request, MeltQuoteBolt12Response, MeltQuoteCustomResponse,
+    MintQuoteBolt12Request, MintQuoteBolt12Response,
 };
 
 use super::Error;
@@ -128,12 +128,12 @@ pub trait MintConnector: Debug {
     async fn post_melt_bolt12_quote(
         &self,
         request: MeltQuoteBolt12Request,
-    ) -> Result<MeltQuoteBolt11Response<String>, Error>;
+    ) -> Result<MeltQuoteBolt12Response<String>, Error>;
     /// Melt Quote Status [NUT-23]
     async fn get_melt_bolt12_quote_status(
         &self,
         quote_id: &str,
-    ) -> Result<MeltQuoteBolt11Response<String>, Error>;
+    ) -> Result<MeltQuoteBolt12Response<String>, Error>;
 
     /// Mint Quote for Custom Payment Method
     async fn post_mint_custom_quote(

+ 43 - 4
crates/cdk/src/wallet/subscription/mod.rs

@@ -155,11 +155,16 @@ impl SubscriptionClient {
     ) -> Option<(usize, String)> {
         let (kind, filter) = match params {
             NotificationId::ProofState(x) => (Kind::ProofState, x.to_string()),
-            NotificationId::MeltQuoteBolt11(q) | NotificationId::MeltQuoteBolt12(q) => {
-                (Kind::Bolt11MeltQuote, q)
-            }
+            NotificationId::MeltQuoteBolt11(q) => (Kind::Bolt11MeltQuote, q),
+            NotificationId::MeltQuoteBolt12(q) => (Kind::Bolt12MeltQuote, q),
             NotificationId::MintQuoteBolt11(q) => (Kind::Bolt11MintQuote, q),
             NotificationId::MintQuoteBolt12(q) => (Kind::Bolt12MintQuote, q),
+            NotificationId::MintQuoteCustom(method, q) => {
+                (Kind::Custom(format!("{}_mint_quote", method)), q)
+            }
+            NotificationId::MeltQuoteCustom(method, q) => {
+                (Kind::Custom(format!("{}_melt_quote", method)), q)
+            }
         };
 
         let request: WsRequest<_> = (
@@ -304,7 +309,41 @@ impl Transport for SubscriptionClient {
                     };
 
                     reply_to.send(MintEvent::new(
-                        NotificationPayload::MeltQuoteBolt11Response(response),
+                        NotificationPayload::MeltQuoteBolt12Response(response),
+                    ));
+                }
+                NotificationId::MintQuoteCustom(method, id) => {
+                    let response = match self
+                        .http_client
+                        .get_mint_quote_custom_status(&method, &id)
+                        .await
+                    {
+                        Ok(success) => success,
+                        Err(err) => {
+                            tracing::error!("Error with Custom Mint Quote {} with {:?}", id, err);
+                            continue;
+                        }
+                    };
+
+                    reply_to.send(MintEvent::new(
+                        NotificationPayload::CustomMintQuoteResponse(method, response),
+                    ));
+                }
+                NotificationId::MeltQuoteCustom(method, id) => {
+                    let response = match self
+                        .http_client
+                        .get_melt_quote_custom_status(&method, &id)
+                        .await
+                    {
+                        Ok(success) => success,
+                        Err(err) => {
+                            tracing::error!("Error with Custom Melt Quote {} with {:?}", id, err);
+                            continue;
+                        }
+                    };
+
+                    reply_to.send(MintEvent::new(
+                        NotificationPayload::CustomMeltQuoteResponse(method, response),
                     ));
                 }
                 _ => {}

+ 2 - 2
crates/cdk/src/wallet/subscription/ws.rs

@@ -148,8 +148,8 @@ pub(crate) async fn stream_client(
                 };
 
                 match msg {
-                    WsMessageOrResponse::Notification(payload) => {
-                        reply_to.send(payload.params.payload);
+                    WsMessageOrResponse::Notification(ref payload) => {
+                        reply_to.send(payload.params.payload.clone());
                     }
                     WsMessageOrResponse::Response(response) => {
                         tracing::debug!("Received response from server: {:?}", response);