Skip to main content

cdk_common/
subscription.rs

1//! Subscription types and traits
2use std::ops::Deref;
3use std::str::FromStr;
4use std::sync::Arc;
5
6use cashu::nut17::{
7    self, Kind, NotificationId, MAX_CUSTOM_KIND_LEN, MAX_FILTER_LEN, MAX_SUBSCRIPTION_ID_LEN,
8};
9use cashu::quote_id::QuoteId;
10use cashu::PublicKey;
11use serde::de::Error as DeError;
12use serde::{Deserialize, Deserializer, Serialize};
13
14use crate::pub_sub::{Error, SubscriptionRequest};
15
16/// CDK/Mint Subscription parameters.
17///
18/// This is a concrete type alias for `nut17::Params<SubId>`.
19pub type Params = nut17::Params<Arc<SubId>>;
20
21impl SubscriptionRequest for Params {
22    type Topic = NotificationId<QuoteId>;
23
24    type SubscriptionId = SubId;
25
26    fn subscription_name(&self) -> Arc<Self::SubscriptionId> {
27        self.id.clone()
28    }
29
30    fn try_get_topics(&self) -> Result<Vec<Self::Topic>, Error> {
31        validate_retained_strings(&self.kind, &self.filters, &self.id)?;
32
33        self.filters
34            .iter()
35            .map(|filter| match self.kind {
36                Kind::Bolt11MeltQuote => QuoteId::from_str(filter)
37                    .map(NotificationId::MeltQuoteBolt11)
38                    .map_err(|_| Error::ParsingError(filter.to_owned())),
39                Kind::Bolt11MintQuote => QuoteId::from_str(filter)
40                    .map(NotificationId::MintQuoteBolt11)
41                    .map_err(|_| Error::ParsingError(filter.to_owned())),
42                Kind::ProofState => PublicKey::from_str(filter)
43                    .map(NotificationId::ProofState)
44                    .map_err(|_| Error::ParsingError(filter.to_owned())),
45
46                Kind::Bolt12MintQuote => QuoteId::from_str(filter)
47                    .map(NotificationId::MintQuoteBolt12)
48                    .map_err(|_| Error::ParsingError(filter.to_owned())),
49                Kind::Bolt12MeltQuote => QuoteId::from_str(filter)
50                    .map(NotificationId::MeltQuoteBolt12)
51                    .map_err(|_| Error::ParsingError(filter.to_owned())),
52                Kind::OnchainMintQuote => QuoteId::from_str(filter)
53                    .map(NotificationId::MintQuoteOnchain)
54                    .map_err(|_| Error::ParsingError(filter.to_owned())),
55                Kind::OnchainMeltQuote => QuoteId::from_str(filter)
56                    .map(NotificationId::MeltQuoteOnchain)
57                    .map_err(|_| Error::ParsingError(filter.to_owned())),
58                Kind::Custom(ref s) => {
59                    if let Some(method) = s.strip_suffix("_mint_quote") {
60                        QuoteId::from_str(filter)
61                            .map(|id| NotificationId::MintQuoteCustom(method.to_string(), id))
62                            .map_err(|_| Error::ParsingError(filter.to_owned()))
63                    } else if let Some(method) = s.strip_suffix("_melt_quote") {
64                        QuoteId::from_str(filter)
65                            .map(|id| NotificationId::MeltQuoteCustom(method.to_string(), id))
66                            .map_err(|_| Error::ParsingError(filter.to_owned()))
67                    } else {
68                        Err(Error::ParsingError(filter.to_owned()))
69                    }
70                }
71            })
72            .collect::<Result<Vec<_>, _>>()
73    }
74}
75
76/// Subscriptions parameters for the wallet
77///
78/// This is because the Wallet can subscribe to non CDK quotes, where IDs are not constraint to
79/// QuoteId
80pub type WalletParams = nut17::Params<Arc<String>>;
81
82impl SubscriptionRequest for WalletParams {
83    type Topic = NotificationId<String>;
84
85    type SubscriptionId = String;
86
87    fn subscription_name(&self) -> Arc<Self::SubscriptionId> {
88        self.id.clone()
89    }
90
91    fn try_get_topics(&self) -> Result<Vec<Self::Topic>, Error> {
92        validate_retained_strings(&self.kind, &self.filters, &self.id)?;
93
94        self.filters
95            .iter()
96            .map(|filter| {
97                Ok(match self.kind {
98                    Kind::Bolt11MeltQuote => NotificationId::MeltQuoteBolt11(filter.to_owned()),
99                    Kind::Bolt11MintQuote => NotificationId::MintQuoteBolt11(filter.to_owned()),
100                    Kind::ProofState => PublicKey::from_str(filter)
101                        .map(NotificationId::ProofState)
102                        .map_err(|_| Error::ParsingError(filter.to_owned()))?,
103
104                    Kind::Bolt12MintQuote => NotificationId::MintQuoteBolt12(filter.to_owned()),
105                    Kind::Bolt12MeltQuote => NotificationId::MeltQuoteBolt12(filter.to_owned()),
106                    Kind::OnchainMintQuote => NotificationId::MintQuoteOnchain(filter.to_owned()),
107                    Kind::OnchainMeltQuote => NotificationId::MeltQuoteOnchain(filter.to_owned()),
108                    Kind::Custom(ref s) => {
109                        if let Some(method) = s.strip_suffix("_mint_quote") {
110                            NotificationId::MintQuoteCustom(method.to_string(), filter.to_owned())
111                        } else if let Some(method) = s.strip_suffix("_melt_quote") {
112                            NotificationId::MeltQuoteCustom(method.to_string(), filter.to_owned())
113                        } else {
114                            // If we can't parse the custom method, we can't create a NotificationId
115                            // This might happen if the custom kind doesn't follow the convention
116                            return Err(Error::ParsingError(format!("Invalid custom kind: {}", s)));
117                        }
118                    }
119                })
120            })
121            .collect::<Result<Vec<_>, _>>()
122    }
123}
124
125fn validate_retained_strings(
126    kind: &Kind,
127    filters: &[String],
128    subscription_id: &str,
129) -> Result<(), Error> {
130    if subscription_id.len() > MAX_SUBSCRIPTION_ID_LEN {
131        return Err(Error::ParsingError(format!(
132            "subscription ID exceeds {MAX_SUBSCRIPTION_ID_LEN} bytes"
133        )));
134    }
135
136    if matches!(kind, Kind::Custom(custom) if custom.len() > MAX_CUSTOM_KIND_LEN) {
137        return Err(Error::ParsingError(format!(
138            "custom subscription kind exceeds {MAX_CUSTOM_KIND_LEN} bytes"
139        )));
140    }
141
142    if filters.iter().any(|filter| filter.len() > MAX_FILTER_LEN) {
143        return Err(Error::ParsingError(format!(
144            "subscription filter exceeds {MAX_FILTER_LEN} bytes"
145        )));
146    }
147
148    Ok(())
149}
150
151/// Subscription Id wrapper
152#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
153pub struct SubId(String);
154
155impl From<&str> for SubId {
156    fn from(s: &str) -> Self {
157        Self(s.to_string())
158    }
159}
160
161impl From<String> for SubId {
162    fn from(s: String) -> Self {
163        Self(s)
164    }
165}
166
167impl FromStr for SubId {
168    type Err = ();
169
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        Ok(Self::from(s))
172    }
173}
174
175impl<'de> Deserialize<'de> for SubId {
176    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177    where
178        D: Deserializer<'de>,
179    {
180        let id = String::deserialize(deserializer)?;
181        if id.len() > MAX_SUBSCRIPTION_ID_LEN {
182            return Err(D::Error::custom(format!(
183                "subscription ID exceeds {MAX_SUBSCRIPTION_ID_LEN} bytes"
184            )));
185        }
186
187        Ok(Self(id))
188    }
189}
190
191impl Deref for SubId {
192    type Target = String;
193
194    fn deref(&self) -> &Self::Target {
195        &self.0
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn subscription_id_length_is_bounded() {
205        let max_length_id = "a".repeat(MAX_SUBSCRIPTION_ID_LEN);
206        let sub_id: SubId = serde_json::from_value(serde_json::json!(max_length_id.clone()))
207            .expect("maximum-length subscription ID");
208        assert_eq!(&*sub_id, &max_length_id);
209        assert_eq!(
210            serde_json::to_value(&sub_id).expect("serialize subscription ID"),
211            serde_json::json!(max_length_id)
212        );
213
214        let oversized_id = "a".repeat(MAX_SUBSCRIPTION_ID_LEN + 1);
215        assert!(serde_json::from_value::<SubId>(serde_json::json!(oversized_id)).is_err());
216    }
217
218    #[test]
219    fn oversized_filter_is_rejected_before_parsing() {
220        let params = Params {
221            kind: Kind::Bolt11MintQuote,
222            filters: vec!["A".repeat(MAX_FILTER_LEN + 4)],
223            id: Arc::new(SubId::from("subscription")),
224        };
225
226        let err = params.try_get_topics().expect_err("oversized filter");
227        assert_eq!(
228            err.to_string(),
229            format!("Parsing Error subscription filter exceeds {MAX_FILTER_LEN} bytes")
230        );
231    }
232
233    #[test]
234    fn maximum_length_wallet_filter_is_accepted() {
235        let filter = "a".repeat(MAX_FILTER_LEN);
236        let params = WalletParams {
237            kind: Kind::Bolt11MintQuote,
238            filters: vec![filter.clone()],
239            id: Arc::new("subscription".to_string()),
240        };
241
242        assert_eq!(
243            params.try_get_topics().expect("maximum-length filter"),
244            vec![NotificationId::MintQuoteBolt11(filter)]
245        );
246    }
247
248    #[test]
249    fn programmatically_constructed_oversized_custom_kind_is_rejected() {
250        let params = Params {
251            kind: Kind::Custom("a".repeat(MAX_CUSTOM_KIND_LEN + 1)),
252            filters: vec![QuoteId::new().to_string()],
253            id: Arc::new(SubId::from("subscription")),
254        };
255
256        let err = params.try_get_topics().expect_err("oversized custom kind");
257        assert_eq!(
258            err.to_string(),
259            format!("Parsing Error custom subscription kind exceeds {MAX_CUSTOM_KIND_LEN} bytes")
260        );
261    }
262
263    #[test]
264    fn programmatically_constructed_oversized_subscription_id_is_rejected() {
265        let params = Params {
266            kind: Kind::Bolt11MintQuote,
267            filters: vec![QuoteId::new().to_string()],
268            id: Arc::new(SubId::from("a".repeat(MAX_SUBSCRIPTION_ID_LEN + 1))),
269        };
270
271        let err = params
272            .try_get_topics()
273            .expect_err("oversized subscription ID");
274        assert_eq!(
275            err.to_string(),
276            format!("Parsing Error subscription ID exceeds {MAX_SUBSCRIPTION_ID_LEN} bytes")
277        );
278
279        let wallet_params = WalletParams {
280            kind: Kind::Bolt11MintQuote,
281            filters: vec!["quote-id".to_string()],
282            id: Arc::new("a".repeat(MAX_SUBSCRIPTION_ID_LEN + 1)),
283        };
284        assert!(wallet_params.try_get_topics().is_err());
285    }
286}