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, nut29, 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)]
21pub struct MintVersion {
22    /// Mint Software name
23    pub name: String,
24    /// Mint Version
25    pub version: String,
26}
27
28impl MintVersion {
29    /// Create new [`MintVersion`]
30    pub fn new(name: String, version: String) -> Self {
31        Self { name, version }
32    }
33}
34
35impl std::fmt::Display for MintVersion {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{}/{}", self.name, self.version)
38    }
39}
40
41impl Serialize for MintVersion {
42    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
43    where
44        S: Serializer,
45    {
46        let combined = format!("{}/{}", self.name, self.version);
47        serializer.serialize_str(&combined)
48    }
49}
50
51impl<'de> Deserialize<'de> for MintVersion {
52    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
53    where
54        D: Deserializer<'de>,
55    {
56        let combined = String::deserialize(deserializer)?;
57        let parts: Vec<&str> = combined.split('/').collect();
58        if parts.len() != 2 {
59            return Err(serde::de::Error::custom("Invalid input string"));
60        }
61        Ok(MintVersion {
62            name: parts[0].to_string(),
63            version: parts[1].to_string(),
64        })
65    }
66}
67
68/// Mint Info [NUT-06]
69#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
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(
76        default,
77        skip_serializing_if = "Option::is_none",
78        deserialize_with = "deserialize_empty_string_as_none"
79    )]
80    pub pubkey: Option<PublicKey>,
81    /// implementation name and the version running
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub version: Option<MintVersion>,
84    /// short description of the mint
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub description: Option<String>,
87    /// long description
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub description_long: Option<String>,
90    /// Contact info
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub contact: Option<Vec<ContactInfo>>,
93    /// shows which NUTs the mint supports
94    pub nuts: Nuts,
95    /// Mint's icon URL
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub icon_url: Option<String>,
98    /// Mint's endpoint URLs
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub urls: Option<Vec<String>>,
101    /// message of the day that the wallet must display to the user
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub motd: Option<String>,
104    /// server unix timestamp
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub time: Option<u64>,
107    /// terms of url service of the mint
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub tos_url: Option<String>,
110}
111
112impl MintInfo {
113    /// Create new [`MintInfo`]
114    pub fn new() -> Self {
115        Self::default()
116    }
117
118    /// Set name
119    pub fn name<S>(self, name: S) -> Self
120    where
121        S: Into<String>,
122    {
123        Self {
124            name: Some(name.into()),
125            ..self
126        }
127    }
128
129    /// Set pubkey
130    pub fn pubkey(self, pubkey: PublicKey) -> Self {
131        Self {
132            pubkey: Some(pubkey),
133            ..self
134        }
135    }
136
137    /// Set [`MintVersion`]
138    pub fn version(self, mint_version: MintVersion) -> Self {
139        Self {
140            version: Some(mint_version),
141            ..self
142        }
143    }
144
145    /// Set description
146    pub fn description<S>(self, description: S) -> Self
147    where
148        S: Into<String>,
149    {
150        Self {
151            description: Some(description.into()),
152            ..self
153        }
154    }
155
156    /// Set long description
157    pub fn long_description<S>(self, description_long: S) -> Self
158    where
159        S: Into<String>,
160    {
161        Self {
162            description_long: Some(description_long.into()),
163            ..self
164        }
165    }
166
167    /// Set contact info
168    pub fn contact_info(self, contact_info: Vec<ContactInfo>) -> Self {
169        Self {
170            contact: Some(contact_info),
171            ..self
172        }
173    }
174
175    /// Set nuts
176    pub fn nuts(self, nuts: Nuts) -> Self {
177        Self { nuts, ..self }
178    }
179
180    /// Set mint icon url
181    pub fn icon_url<S>(self, icon_url: S) -> Self
182    where
183        S: Into<String>,
184    {
185        Self {
186            icon_url: Some(icon_url.into()),
187            ..self
188        }
189    }
190
191    /// Set motd
192    pub fn motd<S>(self, motd: S) -> Self
193    where
194        S: Into<String>,
195    {
196        Self {
197            motd: Some(motd.into()),
198            ..self
199        }
200    }
201
202    /// Set time
203    pub fn time<S>(self, time: S) -> Self
204    where
205        S: Into<u64>,
206    {
207        Self {
208            time: Some(time.into()),
209            ..self
210        }
211    }
212
213    /// Set tos_url
214    pub fn tos_url<S>(self, tos_url: S) -> Self
215    where
216        S: Into<String>,
217    {
218        Self {
219            tos_url: Some(tos_url.into()),
220            ..self
221        }
222    }
223
224    /// Get protected endpoints
225    pub fn protected_endpoints(&self) -> HashMap<ProtectedEndpoint, AuthRequired> {
226        let mut protected_endpoints = HashMap::new();
227
228        if let Some(nut21_settings) = &self.nuts.nut21 {
229            for endpoint in nut21_settings.protected_endpoints.iter() {
230                protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
231            }
232        }
233
234        if let Some(nut22_settings) = &self.nuts.nut22 {
235            for endpoint in nut22_settings.protected_endpoints.iter() {
236                protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
237            }
238        }
239        protected_endpoints
240    }
241
242    /// Get Openid discovery of the mint if it is set
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    pub fn client_id(&self) -> Option<String> {
252        self.nuts.nut21.as_ref().map(|s| s.client_id.clone())
253    }
254
255    /// Max bat mint
256    pub fn bat_max_mint(&self) -> Option<u64> {
257        self.nuts.nut22.as_ref().map(|s| s.bat_max_mint)
258    }
259
260    /// Get all supported currency units for this mint (both mint and melt)
261    pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
262        let mut units = HashSet::new();
263
264        units.extend(self.nuts.supported_mint_units());
265        units.extend(self.nuts.supported_melt_units());
266
267        units.into_iter().collect()
268    }
269}
270
271/// Supported nuts and settings
272#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
273pub struct Nuts {
274    /// NUT04 Settings
275    #[serde(default)]
276    #[serde(rename = "4")]
277    pub nut04: nut04::Settings,
278    /// NUT05 Settings
279    #[serde(default)]
280    #[serde(rename = "5")]
281    pub nut05: nut05::Settings,
282    /// NUT07 Settings
283    #[serde(default)]
284    #[serde(rename = "7")]
285    pub nut07: SupportedSettings,
286    /// NUT08 Settings
287    #[serde(default)]
288    #[serde(rename = "8")]
289    pub nut08: SupportedSettings,
290    /// NUT09 Settings
291    #[serde(default)]
292    #[serde(rename = "9")]
293    pub nut09: SupportedSettings,
294    /// NUT10 Settings
295    #[serde(rename = "10")]
296    #[serde(default)]
297    pub nut10: SupportedSettings,
298    /// NUT11 Settings
299    #[serde(rename = "11")]
300    #[serde(default)]
301    pub nut11: SupportedSettings,
302    /// NUT12 Settings
303    #[serde(default)]
304    #[serde(rename = "12")]
305    pub nut12: SupportedSettings,
306    /// NUT14 Settings
307    #[serde(default)]
308    #[serde(rename = "14")]
309    pub nut14: SupportedSettings,
310    /// NUT15 Settings
311    #[serde(default)]
312    #[serde(rename = "15")]
313    #[serde(skip_serializing_if = "nut15::Settings::is_empty")]
314    pub nut15: nut15::Settings,
315    /// NUT17 Settings
316    #[serde(default)]
317    #[serde(rename = "17")]
318    pub nut17: super::nut17::SupportedSettings,
319    /// NUT19 Settings
320    #[serde(default)]
321    #[serde(rename = "19")]
322    pub nut19: nut19::Settings,
323    /// NUT20 Settings
324    #[serde(default)]
325    #[serde(rename = "20")]
326    pub nut20: SupportedSettings,
327    /// NUT21 Settings
328    #[serde(rename = "21")]
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub nut21: Option<ClearAuthSettings>,
331    /// NUT22 Settings
332    #[serde(rename = "22")]
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub nut22: Option<BlindAuthSettings>,
335    /// NUT29 Settings
336    #[serde(default)]
337    #[serde(rename = "29")]
338    #[serde(skip_serializing_if = "nut29::Settings::is_empty")]
339    pub nut29: nut29::Settings,
340}
341
342impl Nuts {
343    /// Create new [`Nuts`]
344    pub fn new() -> Self {
345        Self::default()
346    }
347
348    /// Nut04 settings
349    pub fn nut04(self, nut04_settings: nut04::Settings) -> Self {
350        Self {
351            nut04: nut04_settings,
352            ..self
353        }
354    }
355
356    /// Nut05 settings
357    pub fn nut05(self, nut05_settings: nut05::Settings) -> Self {
358        Self {
359            nut05: nut05_settings,
360            ..self
361        }
362    }
363
364    /// Nut07 settings
365    pub fn nut07(self, supported: bool) -> Self {
366        Self {
367            nut07: SupportedSettings { supported },
368            ..self
369        }
370    }
371
372    /// Nut08 settings
373    pub fn nut08(self, supported: bool) -> Self {
374        Self {
375            nut08: SupportedSettings { supported },
376            ..self
377        }
378    }
379
380    /// Nut09 settings
381    pub fn nut09(self, supported: bool) -> Self {
382        Self {
383            nut09: SupportedSettings { supported },
384            ..self
385        }
386    }
387
388    /// Nut10 settings
389    pub fn nut10(self, supported: bool) -> Self {
390        Self {
391            nut10: SupportedSettings { supported },
392            ..self
393        }
394    }
395
396    /// Nut11 settings
397    pub fn nut11(self, supported: bool) -> Self {
398        Self {
399            nut11: SupportedSettings { supported },
400            ..self
401        }
402    }
403
404    /// Nut12 settings
405    pub fn nut12(self, supported: bool) -> Self {
406        Self {
407            nut12: SupportedSettings { supported },
408            ..self
409        }
410    }
411
412    /// Nut14 settings
413    pub fn nut14(self, supported: bool) -> Self {
414        Self {
415            nut14: SupportedSettings { supported },
416            ..self
417        }
418    }
419
420    /// Nut15 settings
421    pub fn nut15(self, mpp_settings: Vec<MppMethodSettings>) -> Self {
422        Self {
423            nut15: nut15::Settings {
424                methods: mpp_settings,
425            },
426            ..self
427        }
428    }
429
430    /// Nut17 settings
431    pub fn nut17(self, supported: Vec<SupportedMethods>) -> Self {
432        Self {
433            nut17: super::nut17::SupportedSettings { supported },
434            ..self
435        }
436    }
437
438    /// Nut19 settings
439    pub fn nut19(self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
440        Self {
441            nut19: nut19::Settings {
442                ttl,
443                cached_endpoints,
444            },
445            ..self
446        }
447    }
448
449    /// Nut20 settings
450    pub fn nut20(self, supported: bool) -> Self {
451        Self {
452            nut20: SupportedSettings { supported },
453            ..self
454        }
455    }
456
457    /// Nut29 settings
458    pub fn nut29(self, settings: nut29::Settings) -> Self {
459        Self {
460            nut29: settings,
461            ..self
462        }
463    }
464
465    /// Units where minting is supported
466    pub fn supported_mint_units(&self) -> Vec<&CurrencyUnit> {
467        self.nut04
468            .methods
469            .iter()
470            .map(|s| &s.unit)
471            .collect::<HashSet<_>>()
472            .into_iter()
473            .collect()
474    }
475
476    /// Units where melting is supported
477    pub fn supported_melt_units(&self) -> Vec<&CurrencyUnit> {
478        self.nut05
479            .methods
480            .iter()
481            .map(|s| &s.unit)
482            .collect::<HashSet<_>>()
483            .into_iter()
484            .collect()
485    }
486}
487
488/// Check state Settings
489#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
490pub struct SupportedSettings {
491    /// Setting supported
492    pub supported: bool,
493}
494
495/// Contact Info
496#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
497pub struct ContactInfo {
498    /// Contact Method i.e. nostr
499    pub method: String,
500    /// Contact info i.e. npub...
501    pub info: String,
502}
503
504impl ContactInfo {
505    /// Create new [`ContactInfo`]
506    pub fn new(method: String, info: String) -> Self {
507        Self { method, info }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513
514    use super::*;
515    use crate::nut00::KnownMethod;
516    use crate::nut04::MintMethodOptions;
517    use crate::{Amount, Method, PaymentMethod, RoutePath};
518
519    #[test]
520    fn test_des_mint_into() {
521        let mint_info_str = r#"{
522"name": "Cashu mint",
523"pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679",
524"version": "Nutshell/0.15.3",
525"contact": [
526    ["", ""],
527    ["", ""]
528    ],
529    "nuts": {
530        "4": {
531            "methods": [
532                {"method": "bolt11", "unit": "sat", "description": true},
533                {"method": "bolt11", "unit": "usd", "description": true}
534            ],
535            "disabled": false
536        },
537        "5": {
538            "methods": [
539                {"method": "bolt11", "unit": "sat"},
540                {"method": "bolt11", "unit": "usd"}
541            ],
542            "disabled": false
543        },
544        "7": {"supported": true},
545        "8": {"supported": true},
546        "9": {"supported": true},
547        "10": {"supported": true},
548        "11": {"supported": true}
549    },
550"tos_url": "https://cashu.mint/tos"
551}"#;
552
553        let _mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
554    }
555
556    #[test]
557    fn test_ser_mint_info() {
558        /*
559                let mint_info = serde_json::to_string(&MintInfo {
560                    name: Some("Cashu-crab".to_string()),
561                    pubkey: None,
562                    version: None,
563                    description: Some("A mint".to_string()),
564                    description_long: Some("Some longer test".to_string()),
565                    contact: None,
566                    nuts: Nuts::default(),
567                    motd: None,
568                })
569                .unwrap();
570
571                println!("{}", mint_info);
572        */
573        let mint_info_str = r#"
574{
575  "name": "Bob's Cashu mint",
576  "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
577  "version": "Nutshell/0.15.0",
578  "description": "The short mint description",
579  "description_long": "A description that can be a long piece of text.",
580  "contact": [
581    {
582        "method": "nostr",
583        "info": "xxxxx"
584    },
585    {
586        "method": "email",
587        "info": "contact@me.com"
588    }
589  ],
590  "motd": "Message to display to users.",
591  "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
592  "nuts": {
593    "4": {
594      "methods": [
595        {
596        "method": "bolt11",
597        "unit": "sat",
598        "min_amount": 0,
599        "max_amount": 10000,
600        "options": {
601            "description": true
602            }
603        }
604      ],
605      "disabled": false
606    },
607    "5": {
608      "methods": [
609        {
610        "method": "bolt11",
611        "unit": "sat",
612        "min_amount": 0,
613        "max_amount": 10000
614        }
615      ],
616      "disabled": false
617    },
618    "7": {"supported": true},
619    "8": {"supported": true},
620    "9": {"supported": true},
621    "10": {"supported": true},
622    "12": {"supported": true}
623  },
624  "tos_url": "https://cashu.mint/tos"
625}"#;
626        let info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
627        let mint_info_str = r#"
628{
629    "name": "Bob's Cashu mint",
630    "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
631    "version": "Nutshell/0.15.0",
632    "description": "The short mint description",
633    "description_long": "A description that can be a long piece of text.",
634    "contact": [
635    ["nostr", "xxxxx"],
636    ["email", "contact@me.com"]
637        ],
638        "motd": "Message to display to users.",
639        "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
640        "nuts": {
641            "4": {
642            "methods": [
643                {
644                "method": "bolt11",
645                "unit": "sat",
646                "min_amount": 0,
647                "max_amount": 10000,
648                "options": {
649                     "description": true
650                 }
651                }
652            ],
653            "disabled": false
654            },
655            "5": {
656            "methods": [
657                {
658                "method": "bolt11",
659                "unit": "sat",
660                "min_amount": 0,
661                "max_amount": 10000
662                }
663            ],
664            "disabled": false
665            },
666            "7": {"supported": true},
667            "8": {"supported": true},
668            "9": {"supported": true},
669            "10": {"supported": true},
670            "12": {"supported": true}
671        },
672        "tos_url": "https://cashu.mint/tos"
673}"#;
674        let mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
675
676        let t = mint_info
677            .nuts
678            .nut04
679            .get_settings(
680                &crate::CurrencyUnit::Sat,
681                &crate::PaymentMethod::Known(KnownMethod::Bolt11),
682            )
683            .unwrap();
684
685        let t = t.options.unwrap();
686
687        matches!(t, MintMethodOptions::Bolt11 { description: true });
688
689        assert_eq!(info, mint_info);
690    }
691
692    #[test]
693    fn test_nut15_not_serialized_when_empty() {
694        // Test with default (empty) NUT15
695        let mint_info = MintInfo {
696            name: Some("Test Mint".to_string()),
697            nuts: Nuts::default(),
698            ..Default::default()
699        };
700
701        let json = serde_json::to_string(&mint_info).unwrap();
702        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
703
704        // NUT15 should not be present in the nuts object when methods is empty
705        assert!(parsed["nuts"]["15"].is_null());
706
707        // Test with non-empty NUT15
708        let mint_info_with_nut15 = MintInfo {
709            name: Some("Test Mint".to_string()),
710            nuts: Nuts::default().nut15(vec![MppMethodSettings {
711                method: crate::PaymentMethod::Known(KnownMethod::Bolt11),
712                unit: crate::CurrencyUnit::Sat,
713            }]),
714            ..Default::default()
715        };
716
717        let json = serde_json::to_string(&mint_info_with_nut15).unwrap();
718        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
719
720        // NUT15 should be present when methods is not empty
721        assert!(!parsed["nuts"]["15"].is_null());
722        assert!(parsed["nuts"]["15"]["methods"].is_array());
723        assert_eq!(parsed["nuts"]["15"]["methods"].as_array().unwrap().len(), 1);
724    }
725
726    #[test]
727    fn mint_version_display_uses_name_and_version() {
728        let version = MintVersion::new("cdk".to_string(), "1.2.3".to_string());
729
730        assert_eq!(version.to_string(), "cdk/1.2.3");
731    }
732
733    #[test]
734    fn mint_info_builder_preserves_all_fields() {
735        let pubkey = PublicKey::from_hex(
736            "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
737        )
738        .unwrap();
739        let contact = vec![ContactInfo::new(
740            "email".to_string(),
741            "mint@example.com".to_string(),
742        )];
743        let nuts = Nuts::new().nut07(true).nut08(true);
744
745        let info = MintInfo::new()
746            .name("Test mint")
747            .pubkey(pubkey)
748            .version(MintVersion::new("cdk".to_string(), "1.2.3".to_string()))
749            .description("short")
750            .long_description("long")
751            .contact_info(contact.clone())
752            .nuts(nuts.clone())
753            .icon_url("https://example.com/icon.png")
754            .motd("hello")
755            .time(123_u64)
756            .tos_url("https://example.com/tos");
757
758        assert_eq!(info.name.as_deref(), Some("Test mint"));
759        assert_eq!(info.pubkey, Some(pubkey));
760        assert_eq!(
761            info.version.as_ref().map(ToString::to_string).as_deref(),
762            Some("cdk/1.2.3")
763        );
764        assert_eq!(info.description.as_deref(), Some("short"));
765        assert_eq!(info.description_long.as_deref(), Some("long"));
766        assert_eq!(info.contact, Some(contact));
767        assert_eq!(info.nuts, nuts);
768        assert_eq!(
769            info.icon_url.as_deref(),
770            Some("https://example.com/icon.png")
771        );
772        assert_eq!(info.motd.as_deref(), Some("hello"));
773        assert_eq!(info.time, Some(123));
774        assert_eq!(info.tos_url.as_deref(), Some("https://example.com/tos"));
775    }
776
777    #[test]
778    fn mint_info_auth_helpers_return_configured_values() {
779        let clear_endpoint = ProtectedEndpoint::new(Method::Get, RoutePath::Swap);
780        let blind_endpoint = ProtectedEndpoint::new(
781            Method::Post,
782            RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
783        );
784        let info = MintInfo {
785            nuts: Nuts {
786                nut21: Some(ClearAuthSettings::new(
787                    "https://issuer.example/.well-known/openid-configuration".to_string(),
788                    "wallet-client".to_string(),
789                    vec![clear_endpoint.clone()],
790                )),
791                nut22: Some(BlindAuthSettings::new(42, vec![blind_endpoint.clone()])),
792                ..Default::default()
793            },
794            ..Default::default()
795        };
796
797        let protected = info.protected_endpoints();
798
799        assert_eq!(
800            info.openid_discovery().as_deref(),
801            Some("https://issuer.example/.well-known/openid-configuration")
802        );
803        assert_eq!(info.client_id().as_deref(), Some("wallet-client"));
804        assert_eq!(info.bat_max_mint(), Some(42));
805        assert_eq!(protected.get(&clear_endpoint), Some(&AuthRequired::Clear));
806        assert_eq!(protected.get(&blind_endpoint), Some(&AuthRequired::Blind));
807    }
808
809    #[test]
810    fn nuts_builder_preserves_capabilities_and_supported_units() {
811        let bolt11 = PaymentMethod::Known(KnownMethod::Bolt11);
812        let bolt12 = PaymentMethod::Known(KnownMethod::Bolt12);
813        let mint_settings = nut04::Settings::new(
814            vec![nut04::MintMethodSettings {
815                method: bolt11.clone(),
816                unit: CurrencyUnit::Eur,
817                method_name: Some("Lightning".to_string()),
818                min_amount: Some(Amount::from(1)),
819                max_amount: Some(Amount::from(10)),
820                options: Some(MintMethodOptions::Bolt11 { description: true }),
821            }],
822            false,
823        );
824        let melt_settings = nut05::Settings::new(
825            vec![nut05::MeltMethodSettings {
826                method: bolt12,
827                unit: CurrencyUnit::Usd,
828                method_name: None,
829                min_amount: Some(Amount::from(2)),
830                max_amount: Some(Amount::from(20)),
831                options: None,
832            }],
833            false,
834        );
835        let mpp = vec![MppMethodSettings {
836            method: bolt11.clone(),
837            unit: CurrencyUnit::Sat,
838        }];
839        let supported_ws = vec![SupportedMethods::default_bolt11(CurrencyUnit::Sat)];
840        let cached = vec![CachedEndpoint::new(nut19::Method::Get, nut19::Path::Swap)];
841        let nut29 = nut29::Settings::new(Some(3), Some(vec!["bolt11".to_string()]));
842
843        let nuts = Nuts::new()
844            .nut04(mint_settings.clone())
845            .nut05(melt_settings.clone())
846            .nut07(true)
847            .nut08(true)
848            .nut09(true)
849            .nut10(true)
850            .nut11(true)
851            .nut12(true)
852            .nut14(true)
853            .nut15(mpp.clone())
854            .nut17(supported_ws.clone())
855            .nut19(Some(60), cached.clone())
856            .nut20(true)
857            .nut29(nut29.clone());
858
859        assert_eq!(nuts.nut04, mint_settings);
860        assert_eq!(nuts.nut05, melt_settings);
861        assert!(nuts.nut07.supported);
862        assert!(nuts.nut08.supported);
863        assert!(nuts.nut09.supported);
864        assert!(nuts.nut10.supported);
865        assert!(nuts.nut11.supported);
866        assert!(nuts.nut12.supported);
867        assert!(nuts.nut14.supported);
868        assert_eq!(nuts.nut15.methods, mpp);
869        assert_eq!(nuts.nut17.supported, supported_ws);
870        assert_eq!(nuts.nut19.ttl, Some(60));
871        assert_eq!(nuts.nut19.cached_endpoints, cached);
872        assert!(nuts.nut20.supported);
873        assert_eq!(nuts.nut29, nut29);
874        assert!(nuts.supported_mint_units().contains(&&CurrencyUnit::Eur));
875        assert!(nuts.supported_melt_units().contains(&&CurrencyUnit::Usd));
876
877        let info = MintInfo {
878            nuts,
879            ..Default::default()
880        };
881        let supported_units = info.supported_units();
882        assert!(supported_units.contains(&&CurrencyUnit::Eur));
883        assert!(supported_units.contains(&&CurrencyUnit::Usd));
884    }
885}