1#[cfg(feature = "auth")]
6use std::collections::HashMap;
7use std::collections::HashSet;
8
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11use super::nut01::PublicKey;
12use super::nut17::SupportedMethods;
13use super::nut19::CachedEndpoint;
14use super::{nut04, nut05, nut15, nut19, MppMethodSettings};
15#[cfg(feature = "auth")]
16use super::{AuthRequired, BlindAuthSettings, ClearAuthSettings, ProtectedEndpoint};
17use crate::CurrencyUnit;
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
22pub struct MintVersion {
23 pub name: String,
25 pub version: String,
27}
28
29impl MintVersion {
30 pub fn new(name: String, version: String) -> Self {
32 Self { name, version }
33 }
34}
35
36impl std::fmt::Display for MintVersion {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(f, "{}/{}", self.name, self.version)
39 }
40}
41
42impl Serialize for MintVersion {
43 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44 where
45 S: Serializer,
46 {
47 let combined = format!("{}/{}", self.name, self.version);
48 serializer.serialize_str(&combined)
49 }
50}
51
52impl<'de> Deserialize<'de> for MintVersion {
53 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54 where
55 D: Deserializer<'de>,
56 {
57 let combined = String::deserialize(deserializer)?;
58 let parts: Vec<&str> = combined.split('/').collect();
59 if parts.len() != 2 {
60 return Err(serde::de::Error::custom("Invalid input string"));
61 }
62 Ok(MintVersion {
63 name: parts[0].to_string(),
64 version: parts[1].to_string(),
65 })
66 }
67}
68
69#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
71#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
72pub struct MintInfo {
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub name: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub pubkey: Option<PublicKey>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub version: Option<MintVersion>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub description: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub description_long: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub contact: Option<Vec<ContactInfo>>,
91 pub nuts: Nuts,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub icon_url: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub urls: Option<Vec<String>>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub motd: Option<String>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub time: Option<u64>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub tos_url: Option<String>,
108}
109
110impl MintInfo {
111 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn name<S>(self, name: S) -> Self
118 where
119 S: Into<String>,
120 {
121 Self {
122 name: Some(name.into()),
123 ..self
124 }
125 }
126
127 pub fn pubkey(self, pubkey: PublicKey) -> Self {
129 Self {
130 pubkey: Some(pubkey),
131 ..self
132 }
133 }
134
135 pub fn version(self, mint_version: MintVersion) -> Self {
137 Self {
138 version: Some(mint_version),
139 ..self
140 }
141 }
142
143 pub fn description<S>(self, description: S) -> Self
145 where
146 S: Into<String>,
147 {
148 Self {
149 description: Some(description.into()),
150 ..self
151 }
152 }
153
154 pub fn long_description<S>(self, description_long: S) -> Self
156 where
157 S: Into<String>,
158 {
159 Self {
160 description_long: Some(description_long.into()),
161 ..self
162 }
163 }
164
165 pub fn contact_info(self, contact_info: Vec<ContactInfo>) -> Self {
167 Self {
168 contact: Some(contact_info),
169 ..self
170 }
171 }
172
173 pub fn nuts(self, nuts: Nuts) -> Self {
175 Self { nuts, ..self }
176 }
177
178 pub fn icon_url<S>(self, icon_url: S) -> Self
180 where
181 S: Into<String>,
182 {
183 Self {
184 icon_url: Some(icon_url.into()),
185 ..self
186 }
187 }
188
189 pub fn motd<S>(self, motd: S) -> Self
191 where
192 S: Into<String>,
193 {
194 Self {
195 motd: Some(motd.into()),
196 ..self
197 }
198 }
199
200 pub fn time<S>(self, time: S) -> Self
202 where
203 S: Into<u64>,
204 {
205 Self {
206 time: Some(time.into()),
207 ..self
208 }
209 }
210
211 pub fn tos_url<S>(self, tos_url: S) -> Self
213 where
214 S: Into<String>,
215 {
216 Self {
217 tos_url: Some(tos_url.into()),
218 ..self
219 }
220 }
221
222 #[cfg(feature = "auth")]
224 pub fn protected_endpoints(&self) -> HashMap<ProtectedEndpoint, AuthRequired> {
225 let mut protected_endpoints = HashMap::new();
226
227 if let Some(nut21_settings) = &self.nuts.nut21 {
228 for endpoint in nut21_settings.protected_endpoints.iter() {
229 protected_endpoints.insert(*endpoint, AuthRequired::Clear);
230 }
231 }
232
233 if let Some(nut22_settings) = &self.nuts.nut22 {
234 for endpoint in nut22_settings.protected_endpoints.iter() {
235 protected_endpoints.insert(*endpoint, AuthRequired::Blind);
236 }
237 }
238 protected_endpoints
239 }
240
241 #[cfg(feature = "auth")]
243 pub fn openid_discovery(&self) -> Option<String> {
244 self.nuts
245 .nut21
246 .as_ref()
247 .map(|s| s.openid_discovery.to_string())
248 }
249
250 #[cfg(feature = "auth")]
252 pub fn client_id(&self) -> Option<String> {
253 self.nuts.nut21.as_ref().map(|s| s.client_id.clone())
254 }
255
256 #[cfg(feature = "auth")]
258 pub fn bat_max_mint(&self) -> Option<u64> {
259 self.nuts.nut22.as_ref().map(|s| s.bat_max_mint)
260 }
261
262 pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
264 let mut units = HashSet::new();
265
266 units.extend(self.nuts.supported_mint_units());
267 units.extend(self.nuts.supported_melt_units());
268
269 units.into_iter().collect()
270 }
271}
272
273#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
275#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
276pub struct Nuts {
277 #[serde(default)]
279 #[serde(rename = "4")]
280 pub nut04: nut04::Settings,
281 #[serde(default)]
283 #[serde(rename = "5")]
284 pub nut05: nut05::Settings,
285 #[serde(default)]
287 #[serde(rename = "7")]
288 pub nut07: SupportedSettings,
289 #[serde(default)]
291 #[serde(rename = "8")]
292 pub nut08: SupportedSettings,
293 #[serde(default)]
295 #[serde(rename = "9")]
296 pub nut09: SupportedSettings,
297 #[serde(rename = "10")]
299 #[serde(default)]
300 pub nut10: SupportedSettings,
301 #[serde(rename = "11")]
303 #[serde(default)]
304 pub nut11: SupportedSettings,
305 #[serde(default)]
307 #[serde(rename = "12")]
308 pub nut12: SupportedSettings,
309 #[serde(default)]
311 #[serde(rename = "14")]
312 pub nut14: SupportedSettings,
313 #[serde(default)]
315 #[serde(rename = "15")]
316 pub nut15: nut15::Settings,
317 #[serde(default)]
319 #[serde(rename = "17")]
320 pub nut17: super::nut17::SupportedSettings,
321 #[serde(default)]
323 #[serde(rename = "19")]
324 pub nut19: nut19::Settings,
325 #[serde(default)]
327 #[serde(rename = "20")]
328 pub nut20: SupportedSettings,
329 #[serde(rename = "21")]
331 #[serde(skip_serializing_if = "Option::is_none")]
332 #[cfg(feature = "auth")]
333 pub nut21: Option<ClearAuthSettings>,
334 #[serde(rename = "22")]
336 #[serde(skip_serializing_if = "Option::is_none")]
337 #[cfg(feature = "auth")]
338 pub nut22: Option<BlindAuthSettings>,
339}
340
341impl Nuts {
342 pub fn new() -> Self {
344 Self::default()
345 }
346
347 pub fn nut04(self, nut04_settings: nut04::Settings) -> Self {
349 Self {
350 nut04: nut04_settings,
351 ..self
352 }
353 }
354
355 pub fn nut05(self, nut05_settings: nut05::Settings) -> Self {
357 Self {
358 nut05: nut05_settings,
359 ..self
360 }
361 }
362
363 pub fn nut07(self, supported: bool) -> Self {
365 Self {
366 nut07: SupportedSettings { supported },
367 ..self
368 }
369 }
370
371 pub fn nut08(self, supported: bool) -> Self {
373 Self {
374 nut08: SupportedSettings { supported },
375 ..self
376 }
377 }
378
379 pub fn nut09(self, supported: bool) -> Self {
381 Self {
382 nut09: SupportedSettings { supported },
383 ..self
384 }
385 }
386
387 pub fn nut10(self, supported: bool) -> Self {
389 Self {
390 nut10: SupportedSettings { supported },
391 ..self
392 }
393 }
394
395 pub fn nut11(self, supported: bool) -> Self {
397 Self {
398 nut11: SupportedSettings { supported },
399 ..self
400 }
401 }
402
403 pub fn nut12(self, supported: bool) -> Self {
405 Self {
406 nut12: SupportedSettings { supported },
407 ..self
408 }
409 }
410
411 pub fn nut14(self, supported: bool) -> Self {
413 Self {
414 nut14: SupportedSettings { supported },
415 ..self
416 }
417 }
418
419 pub fn nut15(self, mpp_settings: Vec<MppMethodSettings>) -> Self {
421 Self {
422 nut15: nut15::Settings {
423 methods: mpp_settings,
424 },
425 ..self
426 }
427 }
428
429 pub fn nut17(self, supported: Vec<SupportedMethods>) -> Self {
431 Self {
432 nut17: super::nut17::SupportedSettings { supported },
433 ..self
434 }
435 }
436
437 pub fn nut19(self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
439 Self {
440 nut19: nut19::Settings {
441 ttl,
442 cached_endpoints,
443 },
444 ..self
445 }
446 }
447
448 pub fn nut20(self, supported: bool) -> Self {
450 Self {
451 nut20: SupportedSettings { supported },
452 ..self
453 }
454 }
455
456 pub fn supported_mint_units(&self) -> Vec<&CurrencyUnit> {
458 self.nut04
459 .methods
460 .iter()
461 .map(|s| &s.unit)
462 .collect::<HashSet<_>>()
463 .into_iter()
464 .collect()
465 }
466
467 pub fn supported_melt_units(&self) -> Vec<&CurrencyUnit> {
469 self.nut05
470 .methods
471 .iter()
472 .map(|s| &s.unit)
473 .collect::<HashSet<_>>()
474 .into_iter()
475 .collect()
476 }
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
481#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
482pub struct SupportedSettings {
483 pub supported: bool,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
489#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
490pub struct ContactInfo {
491 pub method: String,
493 pub info: String,
495}
496
497impl ContactInfo {
498 pub fn new(method: String, info: String) -> Self {
500 Self { method, info }
501 }
502}
503
504#[cfg(test)]
505mod tests {
506
507 use super::*;
508 use crate::nut04::MintMethodOptions;
509
510 #[test]
511 fn test_des_mint_into() {
512 let mint_info_str = r#"{
513"name": "Cashu mint",
514"pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679",
515"version": "Nutshell/0.15.3",
516"contact": [
517 ["", ""],
518 ["", ""]
519 ],
520 "nuts": {
521 "4": {
522 "methods": [
523 {"method": "bolt11", "unit": "sat", "description": true},
524 {"method": "bolt11", "unit": "usd", "description": true}
525 ],
526 "disabled": false
527 },
528 "5": {
529 "methods": [
530 {"method": "bolt11", "unit": "sat"},
531 {"method": "bolt11", "unit": "usd"}
532 ],
533 "disabled": false
534 },
535 "7": {"supported": true},
536 "8": {"supported": true},
537 "9": {"supported": true},
538 "10": {"supported": true},
539 "11": {"supported": true}
540 },
541"tos_url": "https://cashu.mint/tos"
542}"#;
543
544 let _mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
545 }
546
547 #[test]
548 fn test_ser_mint_info() {
549 let mint_info_str = r#"
565{
566 "name": "Bob's Cashu mint",
567 "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
568 "version": "Nutshell/0.15.0",
569 "description": "The short mint description",
570 "description_long": "A description that can be a long piece of text.",
571 "contact": [
572 {
573 "method": "nostr",
574 "info": "xxxxx"
575 },
576 {
577 "method": "email",
578 "info": "contact@me.com"
579 }
580 ],
581 "motd": "Message to display to users.",
582 "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
583 "nuts": {
584 "4": {
585 "methods": [
586 {
587 "method": "bolt11",
588 "unit": "sat",
589 "min_amount": 0,
590 "max_amount": 10000,
591 "options": {
592 "description": true
593 }
594 }
595 ],
596 "disabled": false
597 },
598 "5": {
599 "methods": [
600 {
601 "method": "bolt11",
602 "unit": "sat",
603 "min_amount": 0,
604 "max_amount": 10000
605 }
606 ],
607 "disabled": false
608 },
609 "7": {"supported": true},
610 "8": {"supported": true},
611 "9": {"supported": true},
612 "10": {"supported": true},
613 "12": {"supported": true}
614 },
615 "tos_url": "https://cashu.mint/tos"
616}"#;
617 let info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
618 let mint_info_str = r#"
619{
620 "name": "Bob's Cashu mint",
621 "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
622 "version": "Nutshell/0.15.0",
623 "description": "The short mint description",
624 "description_long": "A description that can be a long piece of text.",
625 "contact": [
626 ["nostr", "xxxxx"],
627 ["email", "contact@me.com"]
628 ],
629 "motd": "Message to display to users.",
630 "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
631 "nuts": {
632 "4": {
633 "methods": [
634 {
635 "method": "bolt11",
636 "unit": "sat",
637 "min_amount": 0,
638 "max_amount": 10000,
639 "options": {
640 "description": true
641 }
642 }
643 ],
644 "disabled": false
645 },
646 "5": {
647 "methods": [
648 {
649 "method": "bolt11",
650 "unit": "sat",
651 "min_amount": 0,
652 "max_amount": 10000
653 }
654 ],
655 "disabled": false
656 },
657 "7": {"supported": true},
658 "8": {"supported": true},
659 "9": {"supported": true},
660 "10": {"supported": true},
661 "12": {"supported": true}
662 },
663 "tos_url": "https://cashu.mint/tos"
664}"#;
665 let mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
666
667 let t = mint_info
668 .nuts
669 .nut04
670 .get_settings(&crate::CurrencyUnit::Sat, &crate::PaymentMethod::Bolt11)
671 .unwrap();
672
673 let t = t.options.unwrap();
674
675 matches!(t, MintMethodOptions::Bolt11 { description: true });
676
677 assert_eq!(info, mint_info);
678 }
679}