Skip to main content

cashu/nuts/
nut06.rs

1//! NUT-06: Mint Information
2//!
3//! <https://github.com/cashubtc/nuts/blob/main/06.md>
4
5use 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/// 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(
78        default,
79        skip_serializing_if = "Option::is_none",
80        deserialize_with = "deserialize_empty_string_as_none"
81    )]
82    pub pubkey: Option<PublicKey>,
83    /// implementation name and the version running
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub version: Option<MintVersion>,
86    /// short description of the mint
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub description: Option<String>,
89    /// long description
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub description_long: Option<String>,
92    /// Contact info
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub contact: Option<Vec<ContactInfo>>,
95    /// shows which NUTs the mint supports
96    pub nuts: Nuts,
97    /// Mint's icon URL
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub icon_url: Option<String>,
100    /// Mint's endpoint URLs
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub urls: Option<Vec<String>>,
103    /// message of the day that the wallet must display to the user
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub motd: Option<String>,
106    /// server unix timestamp
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub time: Option<u64>,
109    /// terms of url service of the mint
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub tos_url: Option<String>,
112}
113
114impl MintInfo {
115    /// Create new [`MintInfo`]
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// Set name
121    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    /// Set pubkey
132    pub fn pubkey(self, pubkey: PublicKey) -> Self {
133        Self {
134            pubkey: Some(pubkey),
135            ..self
136        }
137    }
138
139    /// Set [`MintVersion`]
140    pub fn version(self, mint_version: MintVersion) -> Self {
141        Self {
142            version: Some(mint_version),
143            ..self
144        }
145    }
146
147    /// Set description
148    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    /// Set long description
159    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    /// Set contact info
170    pub fn contact_info(self, contact_info: Vec<ContactInfo>) -> Self {
171        Self {
172            contact: Some(contact_info),
173            ..self
174        }
175    }
176
177    /// Set nuts
178    pub fn nuts(self, nuts: Nuts) -> Self {
179        Self { nuts, ..self }
180    }
181
182    /// Set mint icon url
183    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    /// Set motd
194    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    /// Set time
205    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    /// Set tos_url
216    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    /// Get protected endpoints
227    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    /// Get Openid discovery of the mint if it is set
245    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    /// Get Openid discovery of the mint if it is set
253    pub fn client_id(&self) -> Option<String> {
254        self.nuts.nut21.as_ref().map(|s| s.client_id.clone())
255    }
256
257    /// Max bat mint
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    #[serde(skip_serializing_if = "nut15::Settings::is_empty")]
317    pub nut15: nut15::Settings,
318    /// NUT17 Settings
319    #[serde(default)]
320    #[serde(rename = "17")]
321    pub nut17: super::nut17::SupportedSettings,
322    /// NUT19 Settings
323    #[serde(default)]
324    #[serde(rename = "19")]
325    pub nut19: nut19::Settings,
326    /// NUT20 Settings
327    #[serde(default)]
328    #[serde(rename = "20")]
329    pub nut20: SupportedSettings,
330    /// NUT21 Settings
331    #[serde(rename = "21")]
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub nut21: Option<ClearAuthSettings>,
334    /// NUT22 Settings
335    #[serde(rename = "22")]
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub nut22: Option<BlindAuthSettings>,
338}
339
340impl Nuts {
341    /// Create new [`Nuts`]
342    pub fn new() -> Self {
343        Self::default()
344    }
345
346    /// Nut04 settings
347    pub fn nut04(self, nut04_settings: nut04::Settings) -> Self {
348        Self {
349            nut04: nut04_settings,
350            ..self
351        }
352    }
353
354    /// Nut05 settings
355    pub fn nut05(self, nut05_settings: nut05::Settings) -> Self {
356        Self {
357            nut05: nut05_settings,
358            ..self
359        }
360    }
361
362    /// Nut07 settings
363    pub fn nut07(self, supported: bool) -> Self {
364        Self {
365            nut07: SupportedSettings { supported },
366            ..self
367        }
368    }
369
370    /// Nut08 settings
371    pub fn nut08(self, supported: bool) -> Self {
372        Self {
373            nut08: SupportedSettings { supported },
374            ..self
375        }
376    }
377
378    /// Nut09 settings
379    pub fn nut09(self, supported: bool) -> Self {
380        Self {
381            nut09: SupportedSettings { supported },
382            ..self
383        }
384    }
385
386    /// Nut10 settings
387    pub fn nut10(self, supported: bool) -> Self {
388        Self {
389            nut10: SupportedSettings { supported },
390            ..self
391        }
392    }
393
394    /// Nut11 settings
395    pub fn nut11(self, supported: bool) -> Self {
396        Self {
397            nut11: SupportedSettings { supported },
398            ..self
399        }
400    }
401
402    /// Nut12 settings
403    pub fn nut12(self, supported: bool) -> Self {
404        Self {
405            nut12: SupportedSettings { supported },
406            ..self
407        }
408    }
409
410    /// Nut14 settings
411    pub fn nut14(self, supported: bool) -> Self {
412        Self {
413            nut14: SupportedSettings { supported },
414            ..self
415        }
416    }
417
418    /// Nut15 settings
419    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    /// Nut17 settings
429    pub fn nut17(self, supported: Vec<SupportedMethods>) -> Self {
430        Self {
431            nut17: super::nut17::SupportedSettings { supported },
432            ..self
433        }
434    }
435
436    /// Nut19 settings
437    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    /// Nut20 settings
448    pub fn nut20(self, supported: bool) -> Self {
449        Self {
450            nut20: SupportedSettings { supported },
451            ..self
452        }
453    }
454
455    /// Units where minting is supported
456    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    /// Units where melting is supported
467    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/// Check state Settings
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
480#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
481pub struct SupportedSettings {
482    /// Setting supported
483    pub supported: bool,
484}
485
486/// Contact Info
487#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
488#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
489pub struct ContactInfo {
490    /// Contact Method i.e. nostr
491    pub method: String,
492    /// Contact info i.e. npub...
493    pub info: String,
494}
495
496impl ContactInfo {
497    /// Create new [`ContactInfo`]
498    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        /*
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(
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        // Test with default (empty) NUT15
686        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        // NUT15 should not be present in the nuts object when methods is empty
696        assert!(parsed["nuts"]["15"].is_null());
697
698        // Test with non-empty NUT15
699        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        // NUT15 should be present when methods is not empty
712        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}