cdk_common/
subscription.rs1use 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
16pub 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
76pub 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 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#[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}