1use std::collections::{HashMap, HashSet};
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use super::nut01::PublicKey;
10use super::nut17::SupportedMethods;
11use super::nut19::CachedEndpoint;
12use super::{
13 nut04, nut05, nut15, nut19, AuthRequired, BlindAuthSettings, ClearAuthSettings,
14 MppMethodSettings, ProtectedEndpoint,
15};
16use crate::util::serde_helpers::deserialize_empty_string_as_none;
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(
78 default,
79 skip_serializing_if = "Option::is_none",
80 deserialize_with = "deserialize_empty_string_as_none"
81 )]
82 pub pubkey: Option<PublicKey>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub version: Option<MintVersion>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub description: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub description_long: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub contact: Option<Vec<ContactInfo>>,
95 pub nuts: Nuts,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub icon_url: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub urls: Option<Vec<String>>,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub motd: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub time: Option<u64>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub tos_url: Option<String>,
112}
113
114impl MintInfo {
115 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn name<S>(self, name: S) -> Self
122 where
123 S: Into<String>,
124 {
125 Self {
126 name: Some(name.into()),
127 ..self
128 }
129 }
130
131 pub fn pubkey(self, pubkey: PublicKey) -> Self {
133 Self {
134 pubkey: Some(pubkey),
135 ..self
136 }
137 }
138
139 pub fn version(self, mint_version: MintVersion) -> Self {
141 Self {
142 version: Some(mint_version),
143 ..self
144 }
145 }
146
147 pub fn description<S>(self, description: S) -> Self
149 where
150 S: Into<String>,
151 {
152 Self {
153 description: Some(description.into()),
154 ..self
155 }
156 }
157
158 pub fn long_description<S>(self, description_long: S) -> Self
160 where
161 S: Into<String>,
162 {
163 Self {
164 description_long: Some(description_long.into()),
165 ..self
166 }
167 }
168
169 pub fn contact_info(self, contact_info: Vec<ContactInfo>) -> Self {
171 Self {
172 contact: Some(contact_info),
173 ..self
174 }
175 }
176
177 pub fn nuts(self, nuts: Nuts) -> Self {
179 Self { nuts, ..self }
180 }
181
182 pub fn icon_url<S>(self, icon_url: S) -> Self
184 where
185 S: Into<String>,
186 {
187 Self {
188 icon_url: Some(icon_url.into()),
189 ..self
190 }
191 }
192
193 pub fn motd<S>(self, motd: S) -> Self
195 where
196 S: Into<String>,
197 {
198 Self {
199 motd: Some(motd.into()),
200 ..self
201 }
202 }
203
204 pub fn time<S>(self, time: S) -> Self
206 where
207 S: Into<u64>,
208 {
209 Self {
210 time: Some(time.into()),
211 ..self
212 }
213 }
214
215 pub fn tos_url<S>(self, tos_url: S) -> Self
217 where
218 S: Into<String>,
219 {
220 Self {
221 tos_url: Some(tos_url.into()),
222 ..self
223 }
224 }
225
226 pub fn protected_endpoints(&self) -> HashMap<ProtectedEndpoint, AuthRequired> {
228 let mut protected_endpoints = HashMap::new();
229
230 if let Some(nut21_settings) = &self.nuts.nut21 {
231 for endpoint in nut21_settings.protected_endpoints.iter() {
232 protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
233 }
234 }
235
236 if let Some(nut22_settings) = &self.nuts.nut22 {
237 for endpoint in nut22_settings.protected_endpoints.iter() {
238 protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
239 }
240 }
241 protected_endpoints
242 }
243
244 pub fn openid_discovery(&self) -> Option<String> {
246 self.nuts
247 .nut21
248 .as_ref()
249 .map(|s| s.openid_discovery.to_string())
250 }
251
252 pub fn client_id(&self) -> Option<String> {
254 self.nuts.nut21.as_ref().map(|s| s.client_id.clone())
255 }
256
257 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 #[serde(skip_serializing_if = "nut15::Settings::is_empty")]
317 pub nut15: nut15::Settings,
318 #[serde(default)]
320 #[serde(rename = "17")]
321 pub nut17: super::nut17::SupportedSettings,
322 #[serde(default)]
324 #[serde(rename = "19")]
325 pub nut19: nut19::Settings,
326 #[serde(default)]
328 #[serde(rename = "20")]
329 pub nut20: SupportedSettings,
330 #[serde(rename = "21")]
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub nut21: Option<ClearAuthSettings>,
334 #[serde(rename = "22")]
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub nut22: Option<BlindAuthSettings>,
338}
339
340impl Nuts {
341 pub fn new() -> Self {
343 Self::default()
344 }
345
346 pub fn nut04(self, nut04_settings: nut04::Settings) -> Self {
348 Self {
349 nut04: nut04_settings,
350 ..self
351 }
352 }
353
354 pub fn nut05(self, nut05_settings: nut05::Settings) -> Self {
356 Self {
357 nut05: nut05_settings,
358 ..self
359 }
360 }
361
362 pub fn nut07(self, supported: bool) -> Self {
364 Self {
365 nut07: SupportedSettings { supported },
366 ..self
367 }
368 }
369
370 pub fn nut08(self, supported: bool) -> Self {
372 Self {
373 nut08: SupportedSettings { supported },
374 ..self
375 }
376 }
377
378 pub fn nut09(self, supported: bool) -> Self {
380 Self {
381 nut09: SupportedSettings { supported },
382 ..self
383 }
384 }
385
386 pub fn nut10(self, supported: bool) -> Self {
388 Self {
389 nut10: SupportedSettings { supported },
390 ..self
391 }
392 }
393
394 pub fn nut11(self, supported: bool) -> Self {
396 Self {
397 nut11: SupportedSettings { supported },
398 ..self
399 }
400 }
401
402 pub fn nut12(self, supported: bool) -> Self {
404 Self {
405 nut12: SupportedSettings { supported },
406 ..self
407 }
408 }
409
410 pub fn nut14(self, supported: bool) -> Self {
412 Self {
413 nut14: SupportedSettings { supported },
414 ..self
415 }
416 }
417
418 pub fn nut15(self, mpp_settings: Vec<MppMethodSettings>) -> Self {
420 Self {
421 nut15: nut15::Settings {
422 methods: mpp_settings,
423 },
424 ..self
425 }
426 }
427
428 pub fn nut17(self, supported: Vec<SupportedMethods>) -> Self {
430 Self {
431 nut17: super::nut17::SupportedSettings { supported },
432 ..self
433 }
434 }
435
436 pub fn nut19(self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
438 Self {
439 nut19: nut19::Settings {
440 ttl,
441 cached_endpoints,
442 },
443 ..self
444 }
445 }
446
447 pub fn nut20(self, supported: bool) -> Self {
449 Self {
450 nut20: SupportedSettings { supported },
451 ..self
452 }
453 }
454
455 pub fn supported_mint_units(&self) -> Vec<&CurrencyUnit> {
457 self.nut04
458 .methods
459 .iter()
460 .map(|s| &s.unit)
461 .collect::<HashSet<_>>()
462 .into_iter()
463 .collect()
464 }
465
466 pub fn supported_melt_units(&self) -> Vec<&CurrencyUnit> {
468 self.nut05
469 .methods
470 .iter()
471 .map(|s| &s.unit)
472 .collect::<HashSet<_>>()
473 .into_iter()
474 .collect()
475 }
476}
477
478#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
480#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
481pub struct SupportedSettings {
482 pub supported: bool,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
488#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
489pub struct ContactInfo {
490 pub method: String,
492 pub info: String,
494}
495
496impl ContactInfo {
497 pub fn new(method: String, info: String) -> Self {
499 Self { method, info }
500 }
501}
502
503#[cfg(test)]
504mod tests {
505
506 use super::*;
507 use crate::nut00::KnownMethod;
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(
671 &crate::CurrencyUnit::Sat,
672 &crate::PaymentMethod::Known(KnownMethod::Bolt11),
673 )
674 .unwrap();
675
676 let t = t.options.unwrap();
677
678 matches!(t, MintMethodOptions::Bolt11 { description: true });
679
680 assert_eq!(info, mint_info);
681 }
682
683 #[test]
684 fn test_nut15_not_serialized_when_empty() {
685 let mint_info = MintInfo {
687 name: Some("Test Mint".to_string()),
688 nuts: Nuts::default(),
689 ..Default::default()
690 };
691
692 let json = serde_json::to_string(&mint_info).unwrap();
693 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
694
695 assert!(parsed["nuts"]["15"].is_null());
697
698 let mint_info_with_nut15 = MintInfo {
700 name: Some("Test Mint".to_string()),
701 nuts: Nuts::default().nut15(vec![MppMethodSettings {
702 method: crate::PaymentMethod::Known(KnownMethod::Bolt11),
703 unit: crate::CurrencyUnit::Sat,
704 }]),
705 ..Default::default()
706 };
707
708 let json = serde_json::to_string(&mint_info_with_nut15).unwrap();
709 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
710
711 assert!(!parsed["nuts"]["15"].is_null());
713 assert!(parsed["nuts"]["15"]["methods"].is_array());
714 assert_eq!(parsed["nuts"]["15"]["methods"].as_array().unwrap().len(), 1);
715 }
716}