cashu/nuts/
nut06.rs

1//! NUT-06: Mint Information
2//!
3//! <https://github.com/cashubtc/nuts/blob/main/06.md>
4
5#[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/// Mint Version
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
22pub struct MintVersion {
23    /// Mint Software name
24    pub name: String,
25    /// Mint Version
26    pub version: String,
27}
28
29impl MintVersion {
30    /// Create new [`MintVersion`]
31    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/// Mint Info [NUT-06]
70#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
71#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
72pub struct MintInfo {
73    /// name of the mint and should be recognizable
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub name: Option<String>,
76    /// hex pubkey of the mint
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub pubkey: Option<PublicKey>,
79    /// implementation name and the version running
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub version: Option<MintVersion>,
82    /// short description of the mint
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub description: Option<String>,
85    /// long description
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub description_long: Option<String>,
88    /// Contact info
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub contact: Option<Vec<ContactInfo>>,
91    /// shows which NUTs the mint supports
92    pub nuts: Nuts,
93    /// Mint's icon URL
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub icon_url: Option<String>,
96    /// Mint's endpoint URLs
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub urls: Option<Vec<String>>,
99    /// message of the day that the wallet must display to the user
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub motd: Option<String>,
102    /// server unix timestamp
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub time: Option<u64>,
105    /// terms of url service of the mint
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub tos_url: Option<String>,
108}
109
110impl MintInfo {
111    /// Create new [`MintInfo`]
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    /// Set name
117    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    /// Set pubkey
128    pub fn pubkey(self, pubkey: PublicKey) -> Self {
129        Self {
130            pubkey: Some(pubkey),
131            ..self
132        }
133    }
134
135    /// Set [`MintVersion`]
136    pub fn version(self, mint_version: MintVersion) -> Self {
137        Self {
138            version: Some(mint_version),
139            ..self
140        }
141    }
142
143    /// Set description
144    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    /// Set long description
155    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    /// Set contact info
166    pub fn contact_info(self, contact_info: Vec<ContactInfo>) -> Self {
167        Self {
168            contact: Some(contact_info),
169            ..self
170        }
171    }
172
173    /// Set nuts
174    pub fn nuts(self, nuts: Nuts) -> Self {
175        Self { nuts, ..self }
176    }
177
178    /// Set mint icon url
179    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    /// Set motd
190    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    /// Set time
201    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    /// Set tos_url
212    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    /// Get protected endpoints
223    #[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    /// Get Openid discovery of the mint if it is set
242    #[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    /// Get Openid discovery of the mint if it is set
251    #[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    /// Max bat mint
257    #[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    /// Get all supported currency units for this mint (both mint and melt)
263    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/// Supported nuts and settings
274#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
275#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
276pub struct Nuts {
277    /// NUT04 Settings
278    #[serde(default)]
279    #[serde(rename = "4")]
280    pub nut04: nut04::Settings,
281    /// NUT05 Settings
282    #[serde(default)]
283    #[serde(rename = "5")]
284    pub nut05: nut05::Settings,
285    /// NUT07 Settings
286    #[serde(default)]
287    #[serde(rename = "7")]
288    pub nut07: SupportedSettings,
289    /// NUT08 Settings
290    #[serde(default)]
291    #[serde(rename = "8")]
292    pub nut08: SupportedSettings,
293    /// NUT09 Settings
294    #[serde(default)]
295    #[serde(rename = "9")]
296    pub nut09: SupportedSettings,
297    /// NUT10 Settings
298    #[serde(rename = "10")]
299    #[serde(default)]
300    pub nut10: SupportedSettings,
301    /// NUT11 Settings
302    #[serde(rename = "11")]
303    #[serde(default)]
304    pub nut11: SupportedSettings,
305    /// NUT12 Settings
306    #[serde(default)]
307    #[serde(rename = "12")]
308    pub nut12: SupportedSettings,
309    /// NUT14 Settings
310    #[serde(default)]
311    #[serde(rename = "14")]
312    pub nut14: SupportedSettings,
313    /// NUT15 Settings
314    #[serde(default)]
315    #[serde(rename = "15")]
316    pub nut15: nut15::Settings,
317    /// NUT17 Settings
318    #[serde(default)]
319    #[serde(rename = "17")]
320    pub nut17: super::nut17::SupportedSettings,
321    /// NUT19 Settings
322    #[serde(default)]
323    #[serde(rename = "19")]
324    pub nut19: nut19::Settings,
325    /// NUT20 Settings
326    #[serde(default)]
327    #[serde(rename = "20")]
328    pub nut20: SupportedSettings,
329    /// NUT21 Settings
330    #[serde(rename = "21")]
331    #[serde(skip_serializing_if = "Option::is_none")]
332    #[cfg(feature = "auth")]
333    pub nut21: Option<ClearAuthSettings>,
334    /// NUT22 Settings
335    #[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    /// Create new [`Nuts`]
343    pub fn new() -> Self {
344        Self::default()
345    }
346
347    /// Nut04 settings
348    pub fn nut04(self, nut04_settings: nut04::Settings) -> Self {
349        Self {
350            nut04: nut04_settings,
351            ..self
352        }
353    }
354
355    /// Nut05 settings
356    pub fn nut05(self, nut05_settings: nut05::Settings) -> Self {
357        Self {
358            nut05: nut05_settings,
359            ..self
360        }
361    }
362
363    /// Nut07 settings
364    pub fn nut07(self, supported: bool) -> Self {
365        Self {
366            nut07: SupportedSettings { supported },
367            ..self
368        }
369    }
370
371    /// Nut08 settings
372    pub fn nut08(self, supported: bool) -> Self {
373        Self {
374            nut08: SupportedSettings { supported },
375            ..self
376        }
377    }
378
379    /// Nut09 settings
380    pub fn nut09(self, supported: bool) -> Self {
381        Self {
382            nut09: SupportedSettings { supported },
383            ..self
384        }
385    }
386
387    /// Nut10 settings
388    pub fn nut10(self, supported: bool) -> Self {
389        Self {
390            nut10: SupportedSettings { supported },
391            ..self
392        }
393    }
394
395    /// Nut11 settings
396    pub fn nut11(self, supported: bool) -> Self {
397        Self {
398            nut11: SupportedSettings { supported },
399            ..self
400        }
401    }
402
403    /// Nut12 settings
404    pub fn nut12(self, supported: bool) -> Self {
405        Self {
406            nut12: SupportedSettings { supported },
407            ..self
408        }
409    }
410
411    /// Nut14 settings
412    pub fn nut14(self, supported: bool) -> Self {
413        Self {
414            nut14: SupportedSettings { supported },
415            ..self
416        }
417    }
418
419    /// Nut15 settings
420    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    /// Nut17 settings
430    pub fn nut17(self, supported: Vec<SupportedMethods>) -> Self {
431        Self {
432            nut17: super::nut17::SupportedSettings { supported },
433            ..self
434        }
435    }
436
437    /// Nut19 settings
438    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    /// Nut20 settings
449    pub fn nut20(self, supported: bool) -> Self {
450        Self {
451            nut20: SupportedSettings { supported },
452            ..self
453        }
454    }
455
456    /// Units where minting is supported
457    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    /// Units where melting is supported
468    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/// Check state Settings
480#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
481#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
482pub struct SupportedSettings {
483    /// Setting supported
484    pub supported: bool,
485}
486
487/// Contact Info
488#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
489#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
490pub struct ContactInfo {
491    /// Contact Method i.e. nostr
492    pub method: String,
493    /// Contact info i.e. npub...
494    pub info: String,
495}
496
497impl ContactInfo {
498    /// Create new [`ContactInfo`]
499    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        /*
550                let mint_info = serde_json::to_string(&MintInfo {
551                    name: Some("Cashu-crab".to_string()),
552                    pubkey: None,
553                    version: None,
554                    description: Some("A mint".to_string()),
555                    description_long: Some("Some longer test".to_string()),
556                    contact: None,
557                    nuts: Nuts::default(),
558                    motd: None,
559                })
560                .unwrap();
561
562                println!("{}", mint_info);
563        */
564        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}