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;
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use super::nut01::PublicKey;
11use super::nut17::SupportedMethods;
12use super::nut19::CachedEndpoint;
13use super::{nut04, nut05, nut15, nut19, MppMethodSettings};
14#[cfg(feature = "auth")]
15use super::{AuthRequired, BlindAuthSettings, ClearAuthSettings, ProtectedEndpoint};
16
17/// Mint Version
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
20pub struct MintVersion {
21    /// Mint Software name
22    pub name: String,
23    /// Mint Version
24    pub version: String,
25}
26
27impl MintVersion {
28    /// Create new [`MintVersion`]
29    pub fn new(name: String, version: String) -> Self {
30        Self { name, version }
31    }
32}
33
34impl std::fmt::Display for MintVersion {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        write!(f, "{}/{}", self.name, self.version)
37    }
38}
39
40impl Serialize for MintVersion {
41    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
42    where
43        S: Serializer,
44    {
45        let combined = format!("{}/{}", self.name, self.version);
46        serializer.serialize_str(&combined)
47    }
48}
49
50impl<'de> Deserialize<'de> for MintVersion {
51    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52    where
53        D: Deserializer<'de>,
54    {
55        let combined = String::deserialize(deserializer)?;
56        let parts: Vec<&str> = combined.split('/').collect();
57        if parts.len() != 2 {
58            return Err(serde::de::Error::custom("Invalid input string"));
59        }
60        Ok(MintVersion {
61            name: parts[0].to_string(),
62            version: parts[1].to_string(),
63        })
64    }
65}
66
67/// Mint Info [NUT-06]
68#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
69#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
70pub struct MintInfo {
71    /// name of the mint and should be recognizable
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub name: Option<String>,
74    /// hex pubkey of the mint
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub pubkey: Option<PublicKey>,
77    /// implementation name and the version running
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub version: Option<MintVersion>,
80    /// short description of the mint
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub description: Option<String>,
83    /// long description
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub description_long: Option<String>,
86    /// Contact info
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub contact: Option<Vec<ContactInfo>>,
89    /// shows which NUTs the mint supports
90    pub nuts: Nuts,
91    /// Mint's icon URL
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub icon_url: Option<String>,
94    /// Mint's endpoint URLs
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub urls: Option<Vec<String>>,
97    /// message of the day that the wallet must display to the user
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub motd: Option<String>,
100    /// server unix timestamp
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub time: Option<u64>,
103    /// terms of url service of the mint
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub tos_url: Option<String>,
106}
107
108impl MintInfo {
109    /// Create new [`MintInfo`]
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Set name
115    pub fn name<S>(self, name: S) -> Self
116    where
117        S: Into<String>,
118    {
119        Self {
120            name: Some(name.into()),
121            ..self
122        }
123    }
124
125    /// Set pubkey
126    pub fn pubkey(self, pubkey: PublicKey) -> Self {
127        Self {
128            pubkey: Some(pubkey),
129            ..self
130        }
131    }
132
133    /// Set [`MintVersion`]
134    pub fn version(self, mint_version: MintVersion) -> Self {
135        Self {
136            version: Some(mint_version),
137            ..self
138        }
139    }
140
141    /// Set description
142    pub fn description<S>(self, description: S) -> Self
143    where
144        S: Into<String>,
145    {
146        Self {
147            description: Some(description.into()),
148            ..self
149        }
150    }
151
152    /// Set long description
153    pub fn long_description<S>(self, description_long: S) -> Self
154    where
155        S: Into<String>,
156    {
157        Self {
158            description_long: Some(description_long.into()),
159            ..self
160        }
161    }
162
163    /// Set contact info
164    pub fn contact_info(self, contact_info: Vec<ContactInfo>) -> Self {
165        Self {
166            contact: Some(contact_info),
167            ..self
168        }
169    }
170
171    /// Set nuts
172    pub fn nuts(self, nuts: Nuts) -> Self {
173        Self { nuts, ..self }
174    }
175
176    /// Set mint icon url
177    pub fn icon_url<S>(self, icon_url: S) -> Self
178    where
179        S: Into<String>,
180    {
181        Self {
182            icon_url: Some(icon_url.into()),
183            ..self
184        }
185    }
186
187    /// Set motd
188    pub fn motd<S>(self, motd: S) -> Self
189    where
190        S: Into<String>,
191    {
192        Self {
193            motd: Some(motd.into()),
194            ..self
195        }
196    }
197
198    /// Set time
199    pub fn time<S>(self, time: S) -> Self
200    where
201        S: Into<u64>,
202    {
203        Self {
204            time: Some(time.into()),
205            ..self
206        }
207    }
208
209    /// Set tos_url
210    pub fn tos_url<S>(self, tos_url: S) -> Self
211    where
212        S: Into<String>,
213    {
214        Self {
215            tos_url: Some(tos_url.into()),
216            ..self
217        }
218    }
219
220    /// Get protected endpoints
221    #[cfg(feature = "auth")]
222    pub fn protected_endpoints(&self) -> HashMap<ProtectedEndpoint, AuthRequired> {
223        let mut protected_endpoints = HashMap::new();
224
225        if let Some(nut21_settings) = &self.nuts.nut21 {
226            for endpoint in nut21_settings.protected_endpoints.iter() {
227                protected_endpoints.insert(*endpoint, AuthRequired::Clear);
228            }
229        }
230
231        if let Some(nut22_settings) = &self.nuts.nut22 {
232            for endpoint in nut22_settings.protected_endpoints.iter() {
233                protected_endpoints.insert(*endpoint, AuthRequired::Blind);
234            }
235        }
236        protected_endpoints
237    }
238
239    /// Get Openid discovery of the mint if it is set
240    #[cfg(feature = "auth")]
241    pub fn openid_discovery(&self) -> Option<String> {
242        self.nuts
243            .nut21
244            .as_ref()
245            .map(|s| s.openid_discovery.to_string())
246    }
247
248    /// Get Openid discovery of the mint if it is set
249    #[cfg(feature = "auth")]
250    pub fn client_id(&self) -> Option<String> {
251        self.nuts.nut21.as_ref().map(|s| s.client_id.clone())
252    }
253
254    /// Max bat mint
255    #[cfg(feature = "auth")]
256    pub fn bat_max_mint(&self) -> Option<u64> {
257        self.nuts.nut22.as_ref().map(|s| s.bat_max_mint)
258    }
259}
260
261/// Supported nuts and settings
262#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
263#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
264pub struct Nuts {
265    /// NUT04 Settings
266    #[serde(default)]
267    #[serde(rename = "4")]
268    pub nut04: nut04::Settings,
269    /// NUT05 Settings
270    #[serde(default)]
271    #[serde(rename = "5")]
272    pub nut05: nut05::Settings,
273    /// NUT07 Settings
274    #[serde(default)]
275    #[serde(rename = "7")]
276    pub nut07: SupportedSettings,
277    /// NUT08 Settings
278    #[serde(default)]
279    #[serde(rename = "8")]
280    pub nut08: SupportedSettings,
281    /// NUT09 Settings
282    #[serde(default)]
283    #[serde(rename = "9")]
284    pub nut09: SupportedSettings,
285    /// NUT10 Settings
286    #[serde(rename = "10")]
287    #[serde(default)]
288    pub nut10: SupportedSettings,
289    /// NUT11 Settings
290    #[serde(rename = "11")]
291    #[serde(default)]
292    pub nut11: SupportedSettings,
293    /// NUT12 Settings
294    #[serde(default)]
295    #[serde(rename = "12")]
296    pub nut12: SupportedSettings,
297    /// NUT14 Settings
298    #[serde(default)]
299    #[serde(rename = "14")]
300    pub nut14: SupportedSettings,
301    /// NUT15 Settings
302    #[serde(default)]
303    #[serde(rename = "15")]
304    pub nut15: nut15::Settings,
305    /// NUT17 Settings
306    #[serde(default)]
307    #[serde(rename = "17")]
308    pub nut17: super::nut17::SupportedSettings,
309    /// NUT19 Settings
310    #[serde(default)]
311    #[serde(rename = "19")]
312    pub nut19: nut19::Settings,
313    /// NUT20 Settings
314    #[serde(default)]
315    #[serde(rename = "20")]
316    pub nut20: SupportedSettings,
317    /// NUT21 Settings
318    #[serde(rename = "21")]
319    #[serde(skip_serializing_if = "Option::is_none")]
320    #[cfg(feature = "auth")]
321    pub nut21: Option<ClearAuthSettings>,
322    /// NUT22 Settings
323    #[serde(rename = "22")]
324    #[serde(skip_serializing_if = "Option::is_none")]
325    #[cfg(feature = "auth")]
326    pub nut22: Option<BlindAuthSettings>,
327}
328
329impl Nuts {
330    /// Create new [`Nuts`]
331    pub fn new() -> Self {
332        Self::default()
333    }
334
335    /// Nut04 settings
336    pub fn nut04(self, nut04_settings: nut04::Settings) -> Self {
337        Self {
338            nut04: nut04_settings,
339            ..self
340        }
341    }
342
343    /// Nut05 settings
344    pub fn nut05(self, nut05_settings: nut05::Settings) -> Self {
345        Self {
346            nut05: nut05_settings,
347            ..self
348        }
349    }
350
351    /// Nut07 settings
352    pub fn nut07(self, supported: bool) -> Self {
353        Self {
354            nut07: SupportedSettings { supported },
355            ..self
356        }
357    }
358
359    /// Nut08 settings
360    pub fn nut08(self, supported: bool) -> Self {
361        Self {
362            nut08: SupportedSettings { supported },
363            ..self
364        }
365    }
366
367    /// Nut09 settings
368    pub fn nut09(self, supported: bool) -> Self {
369        Self {
370            nut09: SupportedSettings { supported },
371            ..self
372        }
373    }
374
375    /// Nut10 settings
376    pub fn nut10(self, supported: bool) -> Self {
377        Self {
378            nut10: SupportedSettings { supported },
379            ..self
380        }
381    }
382
383    /// Nut11 settings
384    pub fn nut11(self, supported: bool) -> Self {
385        Self {
386            nut11: SupportedSettings { supported },
387            ..self
388        }
389    }
390
391    /// Nut12 settings
392    pub fn nut12(self, supported: bool) -> Self {
393        Self {
394            nut12: SupportedSettings { supported },
395            ..self
396        }
397    }
398
399    /// Nut14 settings
400    pub fn nut14(self, supported: bool) -> Self {
401        Self {
402            nut14: SupportedSettings { supported },
403            ..self
404        }
405    }
406
407    /// Nut15 settings
408    pub fn nut15(self, mpp_settings: Vec<MppMethodSettings>) -> Self {
409        Self {
410            nut15: nut15::Settings {
411                methods: mpp_settings,
412            },
413            ..self
414        }
415    }
416
417    /// Nut17 settings
418    pub fn nut17(self, supported: Vec<SupportedMethods>) -> Self {
419        Self {
420            nut17: super::nut17::SupportedSettings { supported },
421            ..self
422        }
423    }
424
425    /// Nut19 settings
426    pub fn nut19(self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
427        Self {
428            nut19: nut19::Settings {
429                ttl,
430                cached_endpoints,
431            },
432            ..self
433        }
434    }
435
436    /// Nut20 settings
437    pub fn nut20(self, supported: bool) -> Self {
438        Self {
439            nut20: SupportedSettings { supported },
440            ..self
441        }
442    }
443}
444
445/// Check state Settings
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
447#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
448pub struct SupportedSettings {
449    supported: bool,
450}
451
452/// Contact Info
453#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
454#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
455pub struct ContactInfo {
456    /// Contact Method i.e. nostr
457    pub method: String,
458    /// Contact info i.e. npub...
459    pub info: String,
460}
461
462impl ContactInfo {
463    /// Create new [`ContactInfo`]
464    pub fn new(method: String, info: String) -> Self {
465        Self { method, info }
466    }
467}
468
469#[cfg(test)]
470mod tests {
471
472    use super::*;
473
474    #[test]
475    fn test_des_mint_into() {
476        let mint_info_str = r#"{
477"name": "Cashu mint",
478"pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679",
479"version": "Nutshell/0.15.3",
480"contact": [
481    ["", ""],
482    ["", ""]
483    ],
484    "nuts": {
485        "4": {
486            "methods": [
487                {"method": "bolt11", "unit": "sat", "description": true},
488                {"method": "bolt11", "unit": "usd", "description": true}
489            ],
490            "disabled": false
491        },
492        "5": {
493            "methods": [
494                {"method": "bolt11", "unit": "sat"},
495                {"method": "bolt11", "unit": "usd"}
496            ],
497            "disabled": false
498        },
499        "7": {"supported": true},
500        "8": {"supported": true},
501        "9": {"supported": true},
502        "10": {"supported": true},
503        "11": {"supported": true}
504    },
505"tos_url": "https://cashu.mint/tos"
506}"#;
507
508        let _mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
509    }
510
511    #[test]
512    fn test_ser_mint_info() {
513        /*
514                let mint_info = serde_json::to_string(&MintInfo {
515                    name: Some("Cashu-crab".to_string()),
516                    pubkey: None,
517                    version: None,
518                    description: Some("A mint".to_string()),
519                    description_long: Some("Some longer test".to_string()),
520                    contact: None,
521                    nuts: Nuts::default(),
522                    motd: None,
523                })
524                .unwrap();
525
526                println!("{}", mint_info);
527        */
528        let mint_info_str = r#"
529{
530  "name": "Bob's Cashu mint",
531  "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
532  "version": "Nutshell/0.15.0",
533  "description": "The short mint description",
534  "description_long": "A description that can be a long piece of text.",
535  "contact": [
536    {
537        "method": "nostr",
538        "info": "xxxxx"
539    },
540    {
541        "method": "email",
542        "info": "contact@me.com"
543    }
544  ],
545  "motd": "Message to display to users.",
546  "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
547  "nuts": {
548    "4": {
549      "methods": [
550        {
551        "method": "bolt11",
552        "unit": "sat",
553        "min_amount": 0,
554        "max_amount": 10000,
555        "description": true
556        }
557      ],
558      "disabled": false
559    },
560    "5": {
561      "methods": [
562        {
563        "method": "bolt11",
564        "unit": "sat",
565        "min_amount": 0,
566        "max_amount": 10000
567        }
568      ],
569      "disabled": false
570    },
571    "7": {"supported": true},
572    "8": {"supported": true},
573    "9": {"supported": true},
574    "10": {"supported": true},
575    "12": {"supported": true}
576  },
577  "tos_url": "https://cashu.mint/tos"
578}"#;
579        let info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
580        let mint_info_str = r#"
581{
582    "name": "Bob's Cashu mint",
583    "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
584    "version": "Nutshell/0.15.0",
585    "description": "The short mint description",
586    "description_long": "A description that can be a long piece of text.",
587    "contact": [
588    ["nostr", "xxxxx"],
589    ["email", "contact@me.com"]
590        ],
591        "motd": "Message to display to users.",
592        "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
593        "nuts": {
594            "4": {
595            "methods": [
596                {
597                "method": "bolt11",
598                "unit": "sat",
599                "min_amount": 0,
600                "max_amount": 10000,
601                "description": true
602                }
603            ],
604            "disabled": false
605            },
606            "5": {
607            "methods": [
608                {
609                "method": "bolt11",
610                "unit": "sat",
611                "min_amount": 0,
612                "max_amount": 10000
613                }
614            ],
615            "disabled": false
616            },
617            "7": {"supported": true},
618            "8": {"supported": true},
619            "9": {"supported": true},
620            "10": {"supported": true},
621            "12": {"supported": true}
622        },
623        "tos_url": "https://cashu.mint/tos"
624}"#;
625        let mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
626
627        assert_eq!(info, mint_info);
628    }
629}